DI
この記事は?
DI について改めて考えてみる。
依存の解決方法: Dependency Injection と Service Locator
ここでは Dependency Injection と Service Locator とは何か?について記載しようと思いますが、基本的には Martin Folower 氏が 2004 年に公開された下記の記事における定義に沿っています。
このメモ執筆は 2022 年時点であり、当初からだいぶ時間も経っているため、名称に対して広く知られている定義と異なっている部分があるかもしれませんが、あくまでこのメモは下記の記事上の解釈に従っています。
問題
特定の監督による映画のリストを提供する機能を持った MovieLister を考える。この機能の実装は以下のようになっていて、探索可能な全映画リストの取得には MovieFinder を利用する。
code:swift
public class MovieLister {
public func movies(directoredBy directorName: String) -> Movie { return self.finder.findAll()
.filter { $0.director == directorName }
}
}
MovieFinder は以下のようなインタフェースとなっており、
code:swift
public protocol MovieFinder {
}
MovieFinder の具象は、以下のように initializer 内で生成されているものとする。ColonDelimitedMovieFinder はテキストファイルを引数に取り、コンマ区切りで映画の情報が記載されたテキストファイルから情報取り出すことができる。
code:swift
public class MovieLister {
private let finder: MovieFinder
public init() {
self.finder = ColonDelimitedMovieFinder("movies1.txt")
}
}
このような実装は、MovieLister の機能を利用したい他のユーザがいて、かつ映画の保存場所がテキストファイルでなく、CSV, XML, DB などのまるで違う場所であった場合に問題となる。ColonDelimitedMovieFinder とは異なる MovieFinder 実装が必要になるけれど、その実装を組み込む口が MovieLister には存在していない。
また、テスタビリティの問題も存在する。MovieLister が ColonDelimitedMovieFinder を直に作成しているため、テスト用のモックなどへの差し替えが行えず、テストが難しくなってしまっている。
MovieLister クラスには MovieFinder の具象 (上記の場合は ColonDelimitedMovieFinder) を参照させず、抽象である MovieFinder のみを参照させておき、具象は後から組み込めるような形式にしておきたい。
Dependency Injection
Dependency Injection (DI) は、具象をを外部から注入するようなパターンのこと、と言えそう。特徴としては、以下が挙げられる。
MovieLister には MovieFinder のみ参照させる
MovieLister には MovieFinder の具象を組み込む口を提供させる
MovieLister に MovieFinder の具象を組み込むのは、MovieLister の外部で行う
2 つ目の「具象を組み込む口を提供させる」の部分はさまざまな方法がある。一番シンプルなのは、initializer から直接具象を受け渡せるようにしておく形式で、広くは Constructor Injection と呼ばれている。
code:swift
public class MovieLister {
private let finder: MovieFinder
public init(finder: MovieFidner) {
self.finder = finder
}
}
MovieLister の利用側には、以下のようなコードが必要となる。
code:swift
let finder = ColonDelimitedMovieFinder("movies1.txt")
let lister = MovieLister(finder: fidner)
このほかにも、DI 用の setter を用意したり、DI 用のインタフェースを定義してそれを実装したり... など、様々な方法が考えられる。DI コンテナを提供するライブラリでは、特定の方法で DI できるようしておくことが求められる場合もある。
Service Locator
Service Locator は具象を解決可能なオブジェクト、あるいは、具象の利用者が自身の内部でそれを解決するようなパターンのこと、と言えそう。特徴としては、以下が挙げられる。
MovieLister には MovieFinder のみを参照させる
MovieLister には Service Locator を参照させる
MovieLister は Service Locator を利用して、MovieFinder の具象を取り出すことができる。
Service Locator は、MovieLister から MovieFinder への具象を取り除く、という点では DI とやることが変わらない。MovieLister に外部から具象を注入するのではなく、MovieLister がその内部で Service Locator を利用して具象を取り出す形式になる、と言う点が異なる。
Service Locator は、例えば以下のような形式となる。
code:swift
public class ServiceLocator {
public static let shared = ServiceLocator()
private let fidner: MovieFinder!
private init() {}
public func load(finder: MovieFidner) {
self.finder = finder
}
public func resolveMovieFidner() -> MovieFinder {
return self.finder
}
}
これを、MovieLister 側から以下のように利用できる。
code:swift
public class MovieLister {
private let finder: MovideFidner = ServiceLocator.shared.resolveMovieFinder()
}
MovieLister の利用側には、以下のようなコードが必要となる。
code:swift
ServiceLocator.shared.load(finder: ColonDelimitedMovieFinder("movies1.txt"))
let lister = MovieLister()
上記の例では ServiceLocator をシングルトンにしたり、MovieLister 内でシングルトンに直にアクセスしたりしているが、別にシングルトンが必須というわけでもない。よりテスタブルな形式にしたいのであれば、ServiceLocator に対して抽象としてインタフェースを用意して、それを DI できるようにしても良い。
Dependency Injection vs Service Locator
DI と Service Locator の比較については、Martin Fowloer 氏の考察がわかりやすい。どちらも疎結合を目指したアプローチであり、唯一の違いは具象の提供方法で、以下のような違いがある。
Dependency Injection
サービスに外部から具象が提供される
サービス自身は具象を得るために何もしなくても良い
Service Locator
サービス内部で具象を解決する
サービス自身が具象を得るために Service Locator にリクエストを送る
DI は、外部との依存関係がコンストラクタやセッターに現れるため、依存性を理解しやすい。一方、Service Locator は依存関係がサービス内部のロジック上に現れるため、DI と比較すると依存性が理解しにくいかもしれない。
DI は外部から具象を提供するため、サービス内部で具象の解決タイミングを決めることができない。例えば、サービス内部で特定の具象をいくつかの設定項目とともに解決したいケースもある。このような場合は、Service Locator を利用して対応できる。
テスタビリティについては変わらない。DI の場合は言わずもがな、外部からモックを注入できるし、Service Locator の場合も、Service Locator 自体を DI 可能にした上で、Service Locator をテスト用のものに差し替えることで対応はできる。
Service Locator は、以下のような点からアンチパターンと言われることが多い。
サービスが本来不要な Service Locator への依存を持つ
特定の抽象への依存ではなく Service Locator への依存に置き換わることで、サービスと抽象との間の依存関係がわかりにくくなる
依存関係が隠蔽される、とも言い換えられる
ただし、後述する protocol composition for DI パターンのような応用を効かせれば、コード量は多くなるものの、疎結合で依存関係もわかりやすい記述も可能になる。
DI にまつわるパターン
Bastard Injection
コンストラクタインジェクションにてデフォルト引数を利用した注入を行うパターンは Bastard Injection と呼ばれるらしい。以下のようなものになる。
code:swift
public class MovieLister {
private let finder: MovieFinder
public init(finder: MovieFinder = ColonDelimitedMovieFinder("movies1.txt")) {
self.finder = finder
}
}
Bastard Injection は、具象の解決をサービス内部で行いつつ、サービス内部のロジックは依然として抽象に依存している。サービスの利用側が具象を提供する必要がないので、簡単にサービスを利用できるというメリットがある。
ColonDelimitedMovieFinder が MovieLister と同一モジュール内に定義されている場合は、このパターンにはさほど問題がないように思う。必要であれば外部から具象を差し替えることができるし、テスト時にもモックを注入することができる。
ただし、ここで参照する具象が別のモジュールに定義されている場合には、サービスが不要なモジュールへの依存を持ってしまうという問題が出てくる。
例えば、MVP アーキテクチャに沿って設計されたアプリケーションを考える。Presenter は抽象としての View に下記のように依存させたい。
code:swift
protocol MyViewProtocol { /* ... */ }
class MyPresenter {
private let view: MyViewProtocol
init(view: MyViewProtocol) { /* ... */ }
}
上記のコード片では、Presenter は View の具象を参照していない。しかし、Bastard Injection を利用して View の具象を利用すると、下記のようになる。
code:swift
import ViewModule
protocol MyViewProtocol { /* ... */ }
class MyPresenter {
private let view: MyViewProtocol
init(view: MyViewProtocol = MyView()) { /* ... */ }
}
(そのような構成の是非はさておき) Model/View/Presenter をそれぞれ別のモジュールとして定義しておいた場合、Bastard Injection していなければ Presenter モジュールから別モジュールへの依存は発生しなかった。しかし、Bastard Injection を利用してしまうと、Presenter モジュールから View モジュールへの依存ができ、本来不要だったモジュール間の依存が発生してしまう。
このようなケースがあるため、Bastard Injection はアンチパターンと呼ばれることが多い。ただし、最初の例の通り、Bastard Injection する対象が同一モジュール内の存在である、もしくは具象と抽象がサービスとは別の同一モジュール内に定義されていて、それをサービスが import としているのであれば、さほど問題にはならないと思う。ただ、依存性逆転のために具象と抽象の定義場所を分けるのはありがちなパターンなので、将来的にそのような変更を行おうとした場合にやりづらくなる可能性はある。
ちなみに、Apple はテストに関する WWDC のビデオ内で、Bastard Injection を行っているコードをサンプルコードとして提示したことがある (だからと言って Bastard Injection をしても OK!ということにはならないが)。
protocol composition for DI
protocol composition for DI と言いつつ、DI というよりは Service Locator に近いパターンといえる。protocol composition 自体は Swift 言語の機能の内の一つで、複数のプロトコル (≒ 型) を & で結合し新たな型を定義できるというもの。
これを DI における煩雑さの低減のために活用するパターンであり、下記で紹介されている。
まず、どのような課題を解消したいのか?を説明するために、以下のようなコンストラクタインジェクションを考える。
code:swift
class FooViewModel {
let imageProvider: ImageProvider
init(..., imageProvider: ImageProvider)
///...
}
この ViewModel をリファクタしていると、下記のように徐々にコンストラクタが肥大化していく可能性がある。
code:swift
class FooViewModel {
let imageProvider: ImageProvider
let articleProvider: ArticleProvider
let persistanceProvider: PersistanceProvider
init(..., imageProvider: ImageProvider, articleProvider: ArticleProvider, persistanceProvider: PersistanceProvider) {
self.imageProvider = imageProvider
self.articleProvider = articleProvider
self.persistanceProvider = persistanceProvider
///...
}
///...
}
これの辛いところは、コンストラクタが肥大化することだけではなく、依存が増えるのに伴って管理する stored property も増えていくという点。同じような ViewModel が増える場合、似たような多くのコードをまた記述する必要がある。かといって、stored property を消すためにシングルトンを参照するようにすると、DI ができなくなってしまう。
そこで、注入する依存各々に対して下記のような protocol を定義し、
code:swift
protocol Has{Dependency} {
var {dependency}: {Dependency} { get }
}
ViewModel への依存の注入を下記のように書き換える。コンストラクタから注入するのは具象そのものではなく、具象を提供する Dependencies になる。この Dependencies は Service Locator といっても差し支えはなさそうに思える。
code:swift
class FooViewModel {
typealias Dependencies = HasImageProvider & HasArticleProvider
let dependencies: Dependencies
init(..., dependencies: Dependencies)
}
Dependencies の定義自体は、下記のようにする。
code:swift
struct AppDependency: HasImageProvider, HasArticleProvider, HasPersistanceProvider {
let imageProvider: ImageProvider
let articleProvider: ArticlesProvider
let persistanceProvider: PersistenceProvider
}
こうすることで、以下のようなメリットを得られる。
依存が変わっても、typealias の定義を書き換えるだけで良い
initializer の変更も不要
Dependencies からは必要な具象のみ取り出すことができるため、疎結合が保たれる
上記の AppDependency にはアプリケーションにおける全ての依存を列挙しても構わない。それを FooViewModel に渡しても、FooViewModel からは必要な依存しか見えない
DI のサポート: DI Container と Factory
DI Container
コンストラクタインジェクションは DI の方法としてはシンプルなものだけれど、具象の解決がサービスの外部に委譲されてしまう。その具象の解決が一箇所に止まれば良いけれど、様々なサービスに依存として注入する必要がある場合には、システム中にその生成知識が散らばってしまう。あるいは、とある具象 A を生成するのには具象 B, C が必要で、さらに具象 B を生成するのには具象 D, E が必要で... というように、特定の具象を生成する手順が複雑な場合、その生成の重い責務がサービスの利用側に求められてしまう。この両者が同時に発生する可能性もあり得る。
このような DI 上の問題を解決する手段の一つとして DI コンテナがある。DI コンテナの明確な定義や用語の始まりは見つけてないけれど、抽象に対する具象を動的に設定しておいて、後から具象を解決できるような仕組みをさしているように思える。ライブラリによっては、サービスを専用の形式に沿って定義しておけば、サービス生成時に自動的に具象を DI してくれるものもあるようだった。
DI Container の例
Swinject
Resolver
Cleanse
needle
Factory vs DI Container
DI Container は Factory パターンで代用もできる。ただし、Factory は抽象と具象の組み合わせを静的に定義するのに対して、DI Container は動的に抽象と具象の組み合わせを設定できる。動的に設定できる、というのがポイントで、生成ロジックの隠蔽がやりやすくなる。という意見もある。