🐕

クリーンアーキテクチャと依存関係逆転の法則

2021/12/06に公開

はじめに

クリーンアーキテクチャのルールを守るために重要な法則である「依存関係逆転の法則」は処理の流れと依存関係を複雑にし、クリーンアーキテクチャを学ぶにあたって難易度が高いと感じたため、自分自身が理解できたことを書いていきます。

アプリケーションの仕様

本稿で利用するアプリケーションはユーザ名とパスワードを含んだリクエストを受けると、ユーザ名とパスワードのペアが正しいか検証し、正しい場合はトークンを返すアプリケーションです。

アプリケーションのソースコードはGitHubにあります。

以下は実行時のデモです。

$ curl -w '\n' 'http://<IP Address>:<Port>/' --data 'name=higuruchi&password=pass' -XPOST
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNjMxNjgxODk3fQ.YEWrN5T2rQyferN2kt2V3up3fW5N030jlDphkzXx7JU"}
$  curl -w '\n' 'http://<IP Address>:<Port>/' --data 'name=higuruchi&password=faultpass' -XPOST
{"message":"unauthorized"}

使用技術

クリーンアーキテクチャって?

クリーンアーキテクチャとはそれぞれの層を分離し、依存の流れを外から中だけの一方向にすることでDBやフレームワークからの独立性を確保することを目的としたアーキテクチャです。図1が有名ですが、正確に4層である必要はなく、「依存性のルール」、「関心の分離」、「依存関係逆転の法則」が重要とされています。

図1

それぞれの層の解説

アプリケーションのディレクトリ構造

./internal以下のディレクトリがそれぞれの層に対応しています。

❯ tree
.
├── README.md
├── cmd
│   └── main.go
├── go.mod
├── go.sum
└── internal
    ├── config
    │   └── cmd
    │       └── root.go
    ├── di
    │   ├── wire.go
    │   └── wire_gen.go
    ├── externalinterface
    │   ├── database
    │   │   ├── database.go
    │   │   └── table.sql
    │   └── server
    │       └── server.go
    ├── interfaceadapter
    │   ├── controller
    │   │   └── user_controller.go
    │   └── repository
    │       ├── model
    │       │   └── user_model.go
    │       ├── user_repository.go
    │       ├── util.go
    │       └── worker
    │           └── database_handler.go
    └── usecase
        ├── model
        │   └── user_model.go
        ├── repository
        │   └── user_repository.go
        └── user_usecase.go

16 directories, 18 files

Enterprise Business Rules

アプリケーションのビジネスロジックを表現するEntityが所属するレイヤーです。
今回のアプリケーションでは利用していませんが、通常はアプリケーションにとって重要なデータの構造体などが定義されます。

Application Business Rules

Entityが持つ振る舞いを表現するためのレイヤーで、Entityに所属するオブジェクトと協調しユースケースを達成します。

Interface Adapter (interfaceadapter)

Frameworks & Driversの層から来たデータを内側の層で扱えるデータの変換にする役割を果たします。

Frameworks & Drivers (externalinterface)

データベースのドライバーやWebフレームワークなどの外部との連携の役割を果たすコードが所属します。
このレイヤーでは利用するミドルウェアやフレームワークによって大きく実装が異なります。そのため、他の層のコードと比較して頻繁に変更される可能性があります。
また、フロントエンドのユーザインタフェースとなるコードもこの層に所属します。

依存関係逆転の法則

クリーンアーキテクチャは図2のように外の層から内の層への依存に限定しているのですが、

図2

Interface Adater層からExternal Interface層のDBにアクセスしたい場合などのように、内側から外側への依存関係を発生させたい場合があります。
その際に直接モジュールなどにアクセスをした場合、外から内側のみの依存という制約を破り、適切でない依存関係が発生します。
これを解消するために依存関係逆転の法則(図3)を用います。

図3

これによって、プログラムないの依存関係を一方向に保つことができ、externalinterface層のコンポーネントをプラグインとして扱うことができるため、DBを変更やフレームワークの変更にも柔軟に対応することができます。

依存関係逆転の法則を説明するために、モジュール間の依存関係が図4のような場合を考えます。
mainモジュール内でMA1モジュールを呼び出し、MA1モジュール内でMB1を呼び出しています。

図4

このような依存関係の場合、MB1のプログラムを変更した際にMA1のプログラムも変更をしなければならない可能性が発生します。

しかし、ここでポリモーフィズムを利用し、MB1を呼び出すことによって、依存関係を逆転させることができます。

図5のようにMB1をインタフェースに依存させ、MA1はインタフェースを介してMB1のモジュールを呼び出すことで、依存関係を逆転させることができ、また、MB1をプラグインとして扱うことができます。そのため、MB1内のプログラムを変更したとしてもMA1に影響を与えることはなく、依存関係によって生じるバグやプログラム破壊を防いでくれます。

図5

図6は依存関係逆転の法則を利用した場合のアプリケーション全体の処理の流れと依存関係を図にしたものです。

図6

作成したアプリケーション内の依存関係逆転の法則を利用しているプログラムの一例をあげます。
usecase層からinterfaceadapter層のモジュールを呼び出す際の処理です。(図7)

図7

./internal/usecase/repository/user_repository.go
// usecase層から呼び出すためにinterfaceadapter層のコンポーネントが満たすべきインタフェースを定義している
package repository

import (
	"github.com/higuruchi/certification-app/internal/usecase/model"
)

type UserRepository interface {
	FindUser(model.User) (bool, error)
}
./internal/user_usecase.go
package usecase

import (
	"fmt"
	"github.com/higuruchi/certification-app/internal/usecase/repository"
	"github.com/higuruchi/certification-app/internal/usecase/model"
)

type userUsecase struct {
    // interfaceadapter層のモジュールを呼び出すためのインタフェースが構造体のメンバとして定義されている
	userRepository repository.UserRepository
}

type UserUsecase interface {
	Login(model.User) (bool, error)
}

// userUsecaseのコンストラクタ
func NewUserUsecase(userRepository repository.UserRepository) UserUsecase {
	return &userUsecase {
		userRepository: userRepository,
	}
}

func (userUsecase *userUsecase) Login(user model.User) (bool, error) {
    // ここでinterfaceadapter層のモジュールをインタフェースを介して呼び出している
	isLogin, err := userUsecase.userRepository.FindUser(user)
	if err != nil {
		return false, fmt.Errorf("calling useUsecase.userRepository.FindUser:%w", err)
	}

	return isLogin, nil
}
./internal/interfaceadapter/repository/user_repository.go

package repository

import (
	"fmt"
	"github.com/higuruchi/certification-app/internal/interfaceadapter/repository/worker"
	"github.com/higuruchi/certification-app/internal/usecase/model"

)

type UserRepository struct {
    // externalinterface層のモジュールを呼び出すためのインタフェースがメンバとして定義されている
	databaseHandler worker.DatabaseHandler
}

// UserRepositoryのコンストラクタ
func NewUserRepository(
	databaseHandler worker.DatabaseHandler,
) *UserRepository {
	return &UserRepository{
		databaseHandler: databaseHandler,
	}
}

// SQL分を構築しSQLを実行するためexternalinterface層のモジュールを呼び出すモジュール
func (userRepository *UserRepository) FindUser(user model.User) (bool, error) {
	sql := `
    SELECT CASE
		WHEN COUNT(*)=1 THEN 1
		ELSE 0
		END
    FROM users
    WHERE name=? AND password=?
`
	rows, err := userRepository.databaseHandler.Query(sql, user.Name, hashPassword(user.Password))
	defer rows.Close()
	if err != nil {
		return false, fmt.Errorf("calling userRepository.databaseHandler.Query: %w", err)
	}

	var isLogin int
	rows.Next()
	if err := rows.Scan(&isLogin); err != nil {
		return false, fmt.Errorf("calling rows.Scan: %w", err)
	}

	if isLogin == 0 {
		return false, nil
	}

	return true, nil
}
GitHubで編集を提案

Discussion