BambooHero

プログラミングと株式投資をメインに色々書きます

ドメイン駆動設計入門の「依存関係逆転の原則」についての話がわかりやすかった

ドメイン駆動設計入門のChapter7に出てくる「依存関係逆転の原則」についての話が個人的にわかりやすかったのでご紹介します。なお、コード例はSwiftで記述します。

依存関係逆転の原則は以下のように定義されています。

A . 上位レベルのモジュールは下位レベルのモジュールに依存してはならない、どちらのモジュールも抽象に依存すべきである。
B . 抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである。

レベルとは入出力からの距離を示していて、高レベルとは人間に近い抽象的な処理、低レベルとは機械に近い具体的な処理のことです(個人的にはこの説明も結構しっくりきた!)。 DDDでいうと、ドメインモデルに近い方が高レベル、インフラに近い方が低レベルです。

Aの定義について

Aについてはリポジトリパターンで説明するのがわかりやすいです。

f:id:bamboohero:20210414015031p:plain:w500

リポジトリパターン適用前は、ドメインロジックを持つ上位レベルのUserApplicationSeriviceが、データストアロジックを持つ下位レベルのUserRepositoryに依存した形になっています。 UserRepositoryが動作するにはDBなどの外部モジュールを必要とするので、UserApplicationSeriviceのテストが容易ではありません。

class UserRepository {
    // MySQLなどの外部DBに接続
}

class UserApplicationSerivice {
    private let userRepository: UserRepository

    init(userRepository: UserRepository) {
        self.userRepository = userRepository
    }
}

リポジトリパターンを適用すると、上位レベルのUserApplicationSeriviceと下位レベルのUserRepositoryが共に抽象であるIUserRepositoryに依存するように変わります。 UserApplicationSeriviceをテストする際はデータストアをインメモリにしたIUserRepositoryの実装を用意することで、簡単にテストできるようになります。

protocol IUserRepository {}

class UserRepository: IUserRepository {
    // MySQLなどの外部DBに接続
}

class InMemoryUserRepository: IUserRepository {
    // インメモリのDBに接続
}

class UserApplicationSerivice {
    private let userRepository: IUserRepository

    init(userRepository: IUserRepository) {
        self.userRepository = userRepository
    }
}

Bの定義について

抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである。

という定義は、リポジトリパターンの実装を見ればまあそうだよな、で終わっちゃうんですが、本書の以下の記述がとても重要かつ原則の意味を正しく表現できているなと思いました。

インターフェースはそれを利用するクライアントが宣言するものであり、主導権はそのクライアントにあります。

つまり、インタフェースは上位レベルのモジュールの都合で決めるべきで、下位レベルのモジュールはそれに従うように実装するということです。

(あまり良い例ではないかもしれませんが)下位レベルの実装の都合でインタフェースを決めてしまうと、何かのオブジェクトを保存するという処理を、(トランザクションにおける)保存とコミットに分けてしまう、なんてこともあるかもしれません(個人的には良い発見だったので、もっと良い例で紹介したい...)。

protocol IUserRepository {
    func save() {}
    func commit() {}
}

class UserRepository: IUserRepository {
    func save() { // DBにオブジェクトを保存する }
    func commit() { // 保存内容をコミットする }
}

上位レベルのモジュールにとって、DBの具体的実装であるトランザクションという概念は関係ありません(あえて関係するときもあるかもしれないけど)。 なので、上位レベルのモジュールの都合で決めると、インタフェースのメソッドはsave()だけで良いはずです。

インタフェースはどのパッケージに属するのか?

この考え方はパッケージ(iOSではフレームワーク)構成を考える上でも重要だと思っています。

パッケージがドメインレイヤーとインフラレイヤーに分かれている場合、UserApplicationSeriviceはドメインレイヤーのパッケージ、UserRepositoryはインフラレイヤーのパッケージに配置します。

では、IUserReposirotyはどこかというと、ドメインレイヤー側に配置するのが正しいはずです。インタフェースの定義は上位レベルであるドメインレイヤー側の都合で決めるべきだからです。