やっと「依存性の逆転(Dependency Inversion)」がわかった


依存性逆転の原則 - Wikipedia

オブジェクト指向設計において、依存性逆転の原則、または依存関係逆転の原則[1](dependency inversion principle) とはソフトウエアモジュールを疎結合に保つための特定の形式を指す用語。この原則に従うとソフトウェアの振る舞いを定義する上位レベルのモジュールから下位レベルモジュールへの従来の依存関係は逆転し、結果として下位レベルモジュールの実装の詳細から上位レベルモジュールを独立に保つことができるようになる。この原則で述べられていることは以下の2つである:[2]
A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
B. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。

正直こういうのを見てもいまいちわかってなかった。

しかし、ようやく理解した。


だめな例

/src/Domain/User/UserRepository.php
<?php
namespace App\Domain\User;

use App\Infrastructure\DBConnection;

class UserRepository {
  public function __construct(DBConnection $conn) {
    $this->conn = $conn;
  }
  public function save(User $user): void {
    $this->conn->table('users')->save($user->toArray());
  }
}
/src/Infrastructure/DBConnection.php
<?php
namespace App\Infrastructure;

class DBConnection {
  /* ... */
}

この作り方だと、 UserRepository がいるパッケージ App\Domain\User は、 App\Infrastructure パッケージを知っている(=use する)必要がある。

しかし、 UserRepository が担う責務は「User に関する状態を永続化したり取得したりすること」だとすれば、 App\Infrastructure パッケージは知る必要がない。つまり、具体的にどうやって永続化するかを知る必要がない。 UserRepository = ビジネスロジックはその状態がDBに保存されようがメモリに保存されようが関係ない のである。

だから、ここで「依存性の逆転」を利用する。

良い例

/src/Domain/User/IUserRepository.php
<?php
namespace App\Domain\User;

interface IUserRepository {
  public function save(User $user): void;
}
/src/Infrastructure/Domain/User/DBUserRepository.php
<?php
namespace App\Infrastructure\Domain\User;

use App\Domain\IUserRepository;

class DBConnection {
  /* ... */
}

class DBUserRepository implements IUserRepository {
  public function __construct(DBConnection $conn) {
    $this->conn = $conn;
  }
  public function save(User $user): void {
    $this->conn->table('users')->save($user->toArray());
  }
}

これで App\Domain パッケージは App\Infrastructure パッケージを知る必要がなくなった。

代わりに、 App\Infrastructure パッケージが App\Domain パッケージを知り、そのパッケージが必要な実装(例えば DBUserRepository)を行う。

このように依存性(use)の逆転を行うことで、 パッケージごとの責務 を適切に分離出来るようになる。この 「パッケージごとの責務」 という部分が重要だということに気づいた。

DDD というより、「パッケージごとの責務分離」を正しく担うことがオブジェクト指向プログラミングのかなり重要なポイントだった。

そして、 interface を用いてパッケージ分離された宣言と実装は、主に DI コンテナ(IoC コンテナ)を使って紐づけられる。 そのための DI コンテナだった

このサンプルは slimphp/Slim-Skeleton を参考にしている。

このため、 Active Record 型の Laravel Eloquent などは App\Infrastructure 層以下に class User extends Model のようなクラスを生やすべきだし、ビジネスロジックは...分離するのが難しい。これが Active Record 型の弱点である。 Doctrine など POPO(Plain Old PHP Object) を使える Data Mapper 型の O/R Mapper なら分離出来るだろう。