5/26 ドメイン駆動設計 Chapter7 7-4
依存関係をコントロールする
- UserApplicationServiceがテスト用のリポジトリを使用して欲しいのか、DBに接続するプロダクション用のリポジトリを利用して欲しいかは時と場合による
- 開発中であれば前者、リリースビルドは後者
- 重要なのは、どのように制御するかという、依存関係をコントロールする手段をここでは学びます
- 開発中であれば前者、リリースビルドは後者
よくない例から。例として開発中にインメモリのリポジトリを利用するという目的を達成することだけを考えた短絡的なコードはこちらです。
class UserApplicationService { private userRepository: IUserRepository; constructor() { this.userRepository = new InMemoryUserRepository(); } // 省略 }
- フィールド(下記説明あり)であるuserRepositoryは抽象型であるが、具体クラスをコンストラクタ内でインスタンス化しているためにUserApplicationServiceはInMemoryUserRepositoryという詳細なオブジェクトに依存してしまっている
- 問題としては、完成したはずのコードに修正を加える必要が出てくることです
- リリース時にプロダクション用リポジトリを利用するように完成したコードを修正する必要が出てくる
- 問題としては、完成したはずのコードに修正を加える必要が出てくることです
フィールドとは
クラスや構造体などのデータ型に属する変数のことを指します。フィールドは、そのデータ型のインスタンス(オブジェクト)が作成されるときにメモリ上に確保されます。フィールドは、そのデータ型のメソッドからアクセスでき、そのデータ型の状態を保持します。フィールドは通常、プライベート(private)または保護(protected)として宣言され、公開(public)メソッドを通じて間接的にアクセスされます。今回の例で言うと、
private userRepository: IUserRepository;
はUserApplicationService
クラスのプライベートフィールドで、IUserRepository
型のオブジェクトを保持します。このフィールドは、UserApplicationService
クラスのメソッドからアクセス可能で、このクラスの状態を保持します。
リリース前に完成したコードを、テスト用のリポジトリからプロダクション用のリポジトリを利用するように修正
class UserApplicationService { private userRepository: IUserRepository; constructor() { // this.userRepository = new InMemoryUserRepository(); this.userRepository = new UserRepository(); } // 省略 }
- 修正はこれだけにとどまらず、この作りが許されているのであれば、他のコードも似たような構造になっているため、テスト用リポジトリからプロダクション用リポジトリを利用するように修正しなくてはならない
- 一度リリースをして、再度、不具合などで原因究明のためにテスト用リポジトリを利用したい場合に、また全てやり直して、リリース時にはプロダクション用に直すという事態になってしまう
7-4-1 ServiceLocatorパターン
- ServiceLocatorと呼ばれるオブジェクトに依存解決先となるオブジェクトを事前に登録しておき、インスタンスが必要となる各所でServiceLocatorを経由してインスタンスを取得するパターン
ServiceLocatorクラスが定義してあるという前提で、、
class UserApplicationService { private userRepository: IUserRepository; constructor() { // this.userRepository = new InMemoryUserRepository(); this.userRepository = serviceLocator.registerService<IUserRepository>(); } // 省略 }
- コンストラクタでServiceLocatorにIUserRepositoryの依存を解決するように依頼している
この依頼に対して返却される実際のインスタンスを事前に登録しておく
const serviceLocator = new ServiceLocator(); serviceLocator.registerService<IUserRepository>('IUserRepository', new InMemoryRepository());
- IUserRepositoryの依存解決が依頼された際にInMemoryRepositoryをインスタンス化して引き渡します
- プロダクション用のリポジトリを利用したい場合は、ServiceLocatorの登録を変更することで対応する
const serviceLocator = new ServiceLocator(); serviceLocator.registerService<IUserRepository>('IUserRepository', new UserRepository());
- IUserRepositoryを要求するオブジェクトが全てServiceLocator経由でインスタンス化していれば、修正箇所は依存関係を設定してる箇所に集約できます
ServiceLocatorパターンはアンチパターンであるとも言われており、以下2つの理由です
- 依存関係が外部から見えづらくなる
- テストの維持が難しくなる
依存関係が外部から見えづらくなる
class UserApplicationService { private userRepository: IUserRepository; constructor() { this.userRepository = ServiceLocator.registerSerivice<IUserRepository>(); } // 省略 }
UserApplicationService
クラスはIUserRepository
に依存しています- それがどの具体的な実装に依存しているのか(
InMemoryUserRepository
やUserRepository
)、またはその依存性がどのタイミングで解決されるのかがコードからは明確には分かりません
テストの維持が難しくなる
- アプリケーションサービスのコードが変更された場合に、ServiceLocatorも変更が必要になり、それがテスト実行するまで、テストが破壊されたという事実に気づけないところが問題である
7-4-2 IoC Containerパターン
IoCとは、Inversion of Control の略で、制御の反転という意味。
- IoC Container(DI Container) を知るにはまず、Dependency Injectionパターンについて知る必要がある
- 依存性の注入という意味
具体例
const userRepository = new InMemoryUserRepository(); const userApplicationService = new UserApplicationService(userRepository);
- UserApplicationServiceはInMemoryUserRepositoryをDIしています
- コンストラクタで依存するオブジェクトを注入しているので、コンストラクタインジェクションとも呼ばれる
- これまで解説でも登場してきたパターンです
- DIパターンはこれ以外にもメソッドインジェクションなど多くのパターンが存在する
- 注入方法はいくつもあるが、いずれも依存するモジュールを外部から注入することに変わりはない
- DIパターンであれば、依存関係の変更に強制力を持たせることができる
class UserApplicationService { constructor(private readonly userRepository: IUserRepository, private readonly fooRepositry: IFooRepository) {} // 省略 }
- UserApplicationServiceでは新しい依存関係を追加するためにコンストラクタに引数が追加されています