私達は、物事を分割して考えることしかできない。
それ故、私達は1つのクラスにすべてを書かず、クラスを分割する。
class FeatureA {
func foo(): String {
let b = FeatureB() // Use `FeatureB`
return b.bar() + "!!"
}
}
class FeatureB {
...
func bar(): String {
// do something
}
}
ここでは、FeatureA
クラスが FeatureB
クラスを利用している。こういう時、 FeatureA
は FeatureB
に依存していると表現される。FeatureB
が存在しなければ、FeatureA
は利用できないからだ。
前述のコードは問題なく動作するが、FeatureA
のユニットテストをしたい時に問題が起こる。FeatureB
をテスト用のモックに差し替えられないのだ。
だから、私達は Protocol という道具を使って、FeatureB
を差し替えられるようにする。
例えば、以下のように。
class FeatureAImpl {
let b: FeatureB
init(b: FeatureB) {
self.b = b
}
func foo(): String {
return b.bar() + "!!"
}
}
// Introduce protocol
protocol FeatureB {
func bar(): String
}
// Implement `FeatureB`
class FeatureBImpl: FeatureB {
...
func bar(): String { ... }
}
インターフェースと実装を分離する意味で、実装クラスの末尾には Impl
という単語を付けるように変更した。
UMLに詳しくない人のために補足しておくと、点線矢印が依存(dependency)、白抜きの三角矢印が実装(implementation)を表す。この記事を読み進める上では、それだけ理解しておけば概ね十分なはずだ。
ここでは FeatureB
という依存性を外部から与えられるになっているが、これは一般的に『DI(依存性注入)』と呼ばれる。この例では、コンストラクタ経由でそれを行えるようにしているため、『コンストラクタインジェクション』などというパターン名が付けられている。
さて、これによって FeatureAImpl
のユニットテストはもはや容易になった。
// Mock for unit-test
class FeatureBMock: FeatureB {
func bar(): String { return "Hello" }
}
let mock = FeatureBMock()
let a = FeatureAImpl(b: mock) // Constructor Injection
XCTAssertEqual("Hello!!", a.foo())
ところで、プロダクションコードにおいて FeatureBImpl
を生成するのはどこでやるべきだろう? 呼び出し元でいちいち生成するのは面倒だし、そもそも我々はテスタビリティのためだけに Protocol を導入したのだから、生成するのは常に FeatureBImpl
のみだ。
だから私達は通常、以下のようにデフォルト引数で用意する。
class FeatureAImpl {
let b: FeatureB
init(b: FeatureB = FeatureBImpl()) { // Bastard Injection
self.b = b
}
}
// Initialize and use
let a = FeatureAImpl()
a.foo()
これは十分うまく機能する。
依然として FeatureB
はユニットテスト用のモックに差し替え可能だし、FeatureAImpl
の利用者に FeatureBImpl
を意識させる必要もないし、ボイラープレート的なコードも存在しない。
ただし、これは DI においてアンチパターンの一種とされており、”Bastard Injection”などと呼ばれている。
この記事のタイトルに含まれているものだ。
モジュール間の結合
しかし、Bastard Injection の何がマズイのだろうか? インターネット上の記事では以下のように説明されることが多いように思う。
FeatureAImpl
は依然としてFeatureBImpl
に依存しており、これは結合度が強い状態である。
あぁ、確かにそのとおりだ。
実際のところ FeatureAImpl
は FeatureBImpl
なしではコンパイルすることができない。もちろん、私達はソフトウェア開発において、結合度が弱いに越したことはないことを理解している。
しかし、具体的にどのようなデメリットが発生するのだろう? 我々は、単に”結合度を弱くしたい”という理由だけで、Bastard Injection の手軽さを手放すようなことはしたくない。それがゼロコストであるならば話は別だが。
結論から言えば、複数のモジュールに分割した時にモジュール間に依存が発生し、それが大きな問題になることがある。他にも細かい問題が起こりうるが、おそらくこれが一番大きいだろうと思う。
例えば、FeatureA
と FeatureB
をそれぞれ A
と B
というモジュールに分割したとする。
// 📦 A
import B // Dependency to B module
class FeatureAImpl {
let b: FeatureB
init(b: FeatureB = FeatureBImpl()) {
self.b = b
}
...
}
// 📦 B
protocol FeatureB { ... }
class FeatureBImpl: FeatureB {
...
}
外部モジュールに公開するためには public
修飾子が必要となるが、本質的なコードに着目できるようにコード例からは除外している。
当然ながらモジュール間には A
→ B
という依存関係が発生する。言い換えると、A
モジュールをビルドするためには、B
モジュールが必須になる。
結果として、ビルド時間が増加したり、モジュールごとに独立して開発する際の妨げになることがある。それはコードベースやモジュールの数が増えるに従って、より現実的な問題として現れてくる。
また、iOSアプリ開発などにおいては、機能(Feature)ごとにモジュールを分割している場合、モジュール間で相互に画面遷移が発生した場合に、循環依存が発生してビルドできないという問題にも遭遇しうる。
もちろん、これが課題になることもあれば、そうでないこともある。
少なくとも小さなプロジェクトであれば、問題が表面化するケースは少ないはずだ。しかし、昨今ではマルチモジュールによる開発も一般的になりつつあり、こうした問題を解決しておけるに越したことはない。
しかし、この問題は Bastard Injection を無くしただけでは解決できないことが分かる。FeatureBImpl
への依存を無くしても、依然として FeatureB
Protocol には依存しているからだ。
では、どのように解決できるのだろう?
依存性逆転の原則
例えば、以下のようにしたらどうだろう?
// 📦 A
class FeatureAImpl {
let b: FeatureB
init(b: FeatureB) { // Remove Bastard Injection
self.b = b
}
...
}
protocol FeatureB { ... } // Moved from B module
// 📦 B
import A // Dependency
class FeatureBImpl: FeatureB { // new Dependency
...
}
このコード例では、Bastard Injection していた箇所をなくし、 FeatureB
Protocol を A
モジュールに移動している。
この変更によって、モジュール間の依存関係は A
→ B
という向きから、B
→ A
という逆の依存関係を持つようになった。
これは『依存性逆転の原則』とも呼ばれており、それを説明する際の一般的なコード例にあたる。
上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。
抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。
しかし、モジュール間の依存関係 B
→ A
は依然として残っており、モジュール単位で独立してビルド・開発することは実現できていない。これを解決するにはどうしたらよいだろうか?
インターフェース用のモジュール
1つの方法として、インターフェース用のモジュールを別途用意するやり方が考えられる。
// 📦 Interface
protocol FeatureA { ... } // new!
protocol FeatureB { ... } // Moved from A module
// 📦 A
import Interface
class FeatureAImpl: FeatureA {
let b: FeatureB
init(b: FeatureB) {
self.b = b
}
...
}
// 📦 B
import Interface
class FeatureBImpl: FeatureB {
...
}
ここでは Interface
というモジュールを新たに用意し、そこに Protocol を移動している。ついでに FeatureA
についても Protocol を導入している。
これによって A
と B
のモジュール間の依存性はなくなり、どちらも Interface
というインターフェースだけを含むモジュールにのみ依存するようになった。
-
A
→Interface
-
B
→Interface
『依存性逆転の原則』の言葉に合わせるなら、”モジュールは双方ともインターフェースという抽象モジュールにのみ依存する”ようになった。
Bastard Injection を無くした代償
さて、これですべての問題が解決したかというとそんなことはない。
Bastard Injection を無くした為、今度は呼び出し元で依存するオブジェクトを生成しなくてはならなくなる。
// 📦 Main
import Interface
import FeatureA
import FeatureB
// Dependency objects
let b = FeatureBImpl()
let a = FeatureAImpl(b: b) // DI
a.foo()
この例では、大したことが無いように見えるが、仮に FeatureAImpl
がより多くの依存オブジェクトを必要としていた場合はどうだろう? あるいは、FeatureBImpl
が依存オブジェクトを必要としていた場合は? さらにその依存オブジェクトが別の依存オブジェクトを必要としていたら?
// 📦 Main
...
// dependency objects
let e = FeatureEImpl()
let d = FeatureDImpl(e: e) // DI
let c = FeatureCImpl()
let b = FeatureBImpl(d: d) // DI
let a = FeatureAImpl(b: b, c: c) // DI
a.foo()
実のところ依存関係は複雑になるのが一般的であり、Bastard Injection を手放した結果として、呼び出し元におけるボイラープレートのコードが大量に増えることになる。
私たちはオブジェクトを組み立てるコードを書きたいわけではない、単にそれを利用したいだけだ。
いっそのこと、こうしたオブジェクトの依存関係を勝手に解決してくれたらいいのに!
過去の賢明な開発者たちも同じように考えた。その結果として『DIコンテナ』というものが生み出された。
DIコンテナ
DIコンテナの考え方はシンプルで、依存関係を事前に定義しておき、オブジェクトを生成する際の依存関係はDIコンテナ側で勝手に解決してしまおうというものだ。
以下は擬似コードだが、DI コンテナの動作イメージを理解するのには役立つはずだ。
let container = DIContainer()
// Register objects
container.register(FeatureAImpl.class)
container.register(FeatureBImpl.class)
// Get object
let a = container.get(for: FeatureA.class)
a.foo()
事前に FeatureAImpl
と FeatureBImpl
を登録しておくことで、FeatureAImpl
→ FeatureBImpl
という依存関係が勝手に解決された FeatureAImpl
が手に入る。
これは依存関係がどれだけ深くネストしていようと変わらない。言い換えると、FeatureA
が欲しいとだけ言えば、依存関係が解決されたオブジェクトが手に入り、我々はそれをただ使うだけだ。
Bastard Injection を利用した時とコード量は殆ど変わらないことが分かるはずだ。
uber/needle
Swift においても DI コンテナはいくつか開発されているが、ここでは Uber がメンテしている needle を紹介しようと思う。
なぜ needle なのかと言えば、私が触ったことがあるのが needle だけという単純な理由からなのだが、needle はコンパイルタイムセーフで、ボイラープレートに相当するコードを自動生成する仕組みを備えており、開発者にとって扱いやすい DI コンテナであろうと思う。
日本においても実プロダクトに導入しているケースもあるようだ。
needleでDI改善に取り組み始めた話
needleでモジュール間の画面遷移を実現した話 REALITY Advent Calendar #17
なお、この記事は needle の入門記事ではないし、もちろん needle の API リファレンスでもない。なんとなく needle の大まかな雰囲気を理解してもらおうという趣旨なので、実際に利用する際はリポジトリの README などを参照して欲しい。
さて、DIコンテナを利用する際は、依存関係の定義とオブジェクトの登録が必要になる。needle では、例えば以下のように書くことができる。
import NeedleFoundation
class RootComponent: BootstrapComponent {
// register object
var b: FeatureB {
FeatureBImpl()
}
// child component
var aComponent: AComponent {
AComponent(parent: self)
}
}
protocol ADependency: Dependency {
var b: FeatureB { get } // depended to FeatureB
}
class AComponent: Component<ADependency> {
// register object
var a: FeatureA {
FeatureAImpl(b: dependency.b) // resolve dependency
}
}
これは初見では複雑に見えるかもしれない。
順に見ていこう。まず、needle を構成する要素の1つである Component
からだ。
Component
という名称がややこしいが、これは一種の名前空間・スコープを表現するものだ。そして、needle は階層的な依存関係定義を推奨しており、Component
は階層的に定義できる。
このコード例では、以下のような名前空間・スコープが生まれていることが分かる。
class RootComponent: BootstrapComponent {
...
// child component
var aComponent: AComponent {
AComponent(parent: self)
}
}
class AComponent: Component<ADependency> {
...
}
この階層的な名前空間に、オブジェクトを配置していくイメージとなる。
次に、needle を構成するもう1つの要素である Dependency
だ。これはその名のとおり依存を表現するもので、今回のコードでは FeatureB
への依存を定義している。
protocol ADependency: Dependency {
var b: FeatureB { get } // Dependency to FeatureB
}
そして、この依存を利用しているのが以下のコードだ。
class AComponent: Component<ADependency> {
var a: FeatureA {
FeatureAImpl(b: dependency.b) // resolve dependency
}
}
すなわち、AComponent
は ADependency
という依存を持っており、より具体的に言うならば FeatureB
に依存している。
この AComponent
には FeatureA
型の a
プロパティが定義されているが、これが DI コンテナへのオブジェクトの登録に相当するコードになる。dependency
というプロパティは Component
に用意されており、ジェネリクスで指定した Dependency
のプロパティにアクセスできる。
こうして、FeatureAImpl
の依存関係を解決して生成することを実現している。
ところで、この dependency.b
で手に入る実体、すなわち FeatureBImpl
はどこで生成されるのだろう? その定義を行っているのが、RootComponent
における以下のコードとなる。
class RootComponent: BootstrapComponent {
// register object
var b: FeatureB {
FeatureBImpl()
}
...
}
まとめると、以下のようにオブジェクトが配置されているイメージとなる。
- RootComponent
b: FeatureB = FeatureBImpl()
- AComponent
a: FeatureAImpl(b: dependency.b)
FeatureAImpl
を生成する際に、dependency.b
とすることで、親の Component である RootComponent
から b
を取り出していることになる。これは階層がどれだけネストしていようと関係なく、needle は祖先のどこかに依存オブジェクトが定義されていれば、自動的に引っ張ってくる。
それはいささか魔法のように聞こえるかもしれないが、needle ではそれに相当するコードを自動生成するコマンドラインツールを提供している。
$ needle generate Sources/needle.generated.swift Sources
このコマンドは Sources
ディレクトリ内のすべてのソースを解析し、実行する際に必要な Sources/needle.generated.swift
を生成する。あとはビルド対象にこのソースを含めれば準備は完了となる。
さて、最後にコンテナからオブジェクトを取得するコードを見てみよう。
起動時にregisterProviderFactories
という関数を一度だけ呼ぶ必要があることにだけ注意しよう。
import NeedleFoundation
registerProviderFactories() // Required
let component = RootComponent() // Create `RootComponent`
let a = component.aComponent.a // Get object
a.foo()
let b = component.b // In the same way
b.bar()
これで無事に Bastard Injection を利用すること無く、目的となるものが手に入った。
needle は複雑に感じただろうか。
しかし、結局のところ以下の繰り返しであり、依存関係の定義とオブジェクトの配置を行っているに過ぎず、慣れれば一種の DSL として受け入れられるはずだ。
-
Component
で(階層的な)名前空間を作る -
Dependency
で Component が依存するものを定義する - 実体となるオブジェクトを生成するプロパティを
Component
階層の祖先のどこかに設置する
また、この記事では説明しなかったが、インスタンスを Singleton として扱えるようにする shared
という便利なメソッドも用意されている。インスタンス生成の責務を DI コンテナに一任できるのは一つのメリットと言える。
ところで、我々はモジュールを分割しているのだった。needle のコードを含め、各コードがどのモジュールに配置されているのか全体のコード例を示す(依然として public
は省略している)。
// 📦 Interface
protocol FeatureA {
func foo(): String
}
protocol FeatureB {
func bar(): String
}
// 📦 Main
import Interface
import A
import B
import NeedleFoundation
class RootComponent: BootstrapComponent {
var b: FeatureB {
FeatureBImpl()
}
var aComponent: AComponent {
AComponent(parent: self)
}
}
registerProviderFactories()
let a = RootComponent().aComponent.a
a.foo()
// 📦 A
import Interface
import NeedleFoundation
protocol ADependency: Dependency {
var b: FeatureB { get }
}
class AComponent: Component<ADependency> {
var a: FeatureA {
FeatureAImpl(b: dependency.b)
}
}
class FeatureAImpl {
let b: FeatureB
init(b: FeatureB) {
self.b = b
}
func foo(): String {
return bar() + "!!"
}
}
// 📦 B
import Interface
class FeatureBImpl: FeatureB {
init() {}
func bar(): String {
// do something
}
}
モジュール間の依存は必要最低限になっており、ボイラープレートのようなコードも殆ど存在しないことが分かる。
Bastard Injection を利用し続けるメリット
さて、この記事では Bastard Injection の問題点と、それをやめた場合にコストとなるオブジェクトの依存解決を DI コンテナによって解決できるという内容を書いてきたが、それでも Bastard Injection を利用し続けるメリットはあるだろうか?
第1に、Bastard Injection は殆どのケースで十分に機能する。テスタビリティは確保できるし、それをやるためのコストも殆どない。実際に Apple は過去の WWDC において、Bastard Injection を利用したテスト方法を紹介している。
第2に、Bastard Injection は驚くほどシンプルだ。コンストラクタのデフォルト引数をサポートしていない言語では面倒だが、それをサポートしている Swift においては最低限のコード量で済む。needle は最低限のコード量で済むようになっており、依存関係を明確化できるというメリットもあるが、それでも管理するコード量は増える。
第3に、コードの追跡がしやすい。実装クラスがコンストラクタのデフォルト引数に書かれているので、Xcode から実装クラスをたどることは容易だ。Bastard Injection を利用しない場合においても、命名規約(Impl
など)によってカバーできるだろうが、コードジャンプはしづらくなるはずだ。
つまるところ、あなたが Bastard Injection による現実的な課題に遭遇していない場合、それを廃止して DI コンテナを利用するように切り替えるべき強い理由はない。
しかし、プロジェクトの後半になってからテストや CI を導入するのに大きな傷みを伴うのと同様、あとから DI コンテナを導入するのにもそれなりの痛みが伴うはずだ。
あなたが新規プロジェクト、あるいは今後発展しうる可能性のある小さなプロジェクトで作業している場合、早めに導入することで、そのコストを最小限にできる可能性はある。
詳細ではなく抽象に依存する
ここまで長々と見てきたが、結局のところ私達は何を手に入れたのだろうか。
最初のコードの問題点は、FeatureAImpl
が FeatureBImpl
という詳細(実装)を知っていたことにある(名前は Impl
付きに変更している)。
class FeatureAImpl {
func foo(): String {
let b = FeatureBImpl() // Depends on details
return b.bar() + "!!"
}
}
class FeatureBImpl {
func bar(): String { ... }
}
それを FeatureB
という抽象に依存するようにし、詳細(実装)である FeatureBImpl
を知らなくすることで、FeatureB
を任意の詳細(実装)に差し替え可能にし、FeatureAImpl
単体でコンパイルすることを可能にした。
class FeatureAImpl {
let b: FeatureB
init(b: FeatureB) { // Depends on abstraction
self.b = b
}
func foo(): String {
return bar() + "!!"
}
}
protocol FeatureB {
func bar(): String
}
class FeatureBImpl: FeatureB {
init() {}
func bar(): String { ... }
}
マルチモジュールにおいては、Interface
という抽象モジュールを用意することで、モジュール間の依存を無くすことで、モジュール単体でのビルドを可能にし、任意のモジュールに差し替えることも可能にした。
// 📦 Interface
protocol FeatureA { ... }
protocol FeatureB { ... }
// 📦 A
import Interface // Depends on abstraction
class FeatureAImpl: FeatureA {
let b: FeatureB
init(b: FeatureB) { // Depends on abstraction
self.b = b
}
...
}
// 📦 B
import Interface // Depends on abstraction
class FeatureBImpl: FeatureB {
func bar(): String { ... }
}
結局のところ、詳細ではなく抽象に依存するように変更したに過ぎない。
上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。
抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。
シングルモジュールにおいては FeatureBImpl
という詳細を剥がすために FeatureB
という抽象を導入し、マルチモジュールにおいては B
モジュールという詳細を剥がすために、Interface
という抽象を導入した。
詳細を知らないようにすることで、私達はそれを差し替え可能にし、独立してビルドすることを可能にする。それをクラス間で行うか、モジュール間で行うか、それだけの違いなのである。
DIコンテナは、単にこれを補完するための仕組みの1つに過ぎない。
まとめ
- Bastard Injection は殆どのケースで十分に機能する。
- ただし、マルチモジュールの場合にモジュール間の依存が発生する。
- モジュール間の依存は、ビルド時間の増大などの様々な問題を引き起こす。
- Bastard Injection を廃止し、依存性逆転の原則を適用することでそれを解消できる。
- オブジェクトを組み立てる処理が大変になるが、DI コンテナによって負担を軽減できる。
- uber/needle は Swift 向けの DIコンテナの1つで、コンパイルタイムセーフである。
- テストや CI と同様、プロジェクトの早期に導入することで、将来的な導入コストを減らせる。
- 詳細を知らないようにすれば、差し替え可能になり、独立してビルドできる。
Q&A - 追記:2022/03/02
Q. FeatureA
とFeatureB
を同一のInterface
モジュールに配置したのはなぜ?
もちろん分けても構わない。
例えば、今回のケースにおいてはInterfaceA
とInterfaceB
という2つのモジュールに分けても全く問題ない。
しかし、InterfaceA
とInterfaceB
に何らかの依存があった場合、インターフェース用のモジュール同士で依存が発生する。
より極端なケースにおいては以下のようなケースも考えられる。
// 📦 InterfaceA
protocol A {
func createB() -> B
}
// 📦 InterfaceB
protocol B {
func createA() -> A
}
これはInterfaceA
とInterfaceB
でモジュール間の循環参照が発生しており、これはSwiftコンパイラによって弾かれる。
cyclic dependency declaration found: A -> B -> InterfaceB -> InterfaceA -> InterfaceB
一方で、循環参照が発生しないように設計するのであれば、import InterfaceXxx
という宣言によって、依存しているインターフェース用のモジュールがソースコード上で明確化されるというメリットも考えられる。
私もインターフェース用のモジュールを分割した経験はないので、このあたりのノウハウをお持ちの方はコメントなりで教えて欲しい。
あとがき
久しぶりの技術記事のためか、翻訳記事のような文体になってしまいましたが、誰かしらの参考になれば幸いです。
なお、私自身 uber/needle を深く触ったわけではない為、この記事では十分に言及できていない可能性があります。例えば、現状で見えている大きな欠点として、associatedtype
を持つプロトコルを DI の対象にできない、といった問題を見つけています。
より実践的に触った際は、あらためて記事に起こしたいと思います。