しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

SwiftにおけるBastard Injectionとは

結構ありがちな書き方なので、書いてみました。

Bastard Injectionとは

Bastard Injection(Poor Man's Dependency Injection)は、他の言語でも言及されることのあるDIのアンチパターンです。

具体的には、interface(protocol)を引数に取ってconstructor injectionを行う際に、 別のconstructorのデフォルトで、そのinterfaceを実装したクラスを渡してしまうパターンのことです。

Swiftのコードで書くとこのようになります。

protocol Animal {
    // ...
}

class Cat: Animal {
    // ...
}

class Foo {
    init(pet: Animal = Cat()) {
        // ...
    }
}

上記のコードの場合、Foo(pet: Animal = Cat())の部分でデフォルト引数でCatを渡してしまっている部分が、Bastard Injectionにあたります。

なぜBastard Injectionが問題となるのか

DIをする動機として、対象のクラスの他クラスへの依存を減らして、インターフェースへの依存のみに留めることで、できるだけ結合を弱くしたいという大きな理由があります。

そのために、Foo(pet: Cat)Foo(pet: Dog)といったクラスに依存したinitializerではなく、Foo(pet: Animal)という形のinitializerを用意するわけですが、上記コードのようなデフォルト引数を追加することでFooCatに依存することになり、結合が強くなってしまうわけです。

この書き方は、上記のDIの利点を潰してしまっているため、ある意味本末転倒とも言えます。

なぜ書いてしまうのか

実は、この書き方はtestabilityの観点では問題ないです。

Fooクラスにオブジェクト接合部(与えるオブジェクトに応じて外部から振る舞いを変えることを可能にする部分)を作るという点では成功しているので、テストを書くことはできます。

class Foo {
    private let pet: Animal

    init() {
        pet = Cat()
    }
}

上記は接合部のないコードです。

この書き方では外部からpetを変更することができませんが、一番最初に書いた例では、

class CatMock: Animal {
    // ...
}

let foo = Foo(pet: CatMock())

などとして、テストを書く際にモックに置き換えることができます。

こういったメリットもあることから、Bastard Injectionとなってしまっているコードは生じやすいです。

置き換える方法

基本的にはデフォルト引数を剥がすだけで問題ありません。

class Foo {
    init(pet: Animal) {
        // ...
    }
}

let foo = Foo(pet: Cat())

このように毎回オブジェクトを生成する側で、引数として渡すのが単純な解決方法になります。

この書き方は引数が増えた際に書くのが面倒になることもあるので、その場合はDIコンテナなどを使ってまとめて管理すると、見通しが良くなります。

その際は、コンテナへの依存を最小限に抑えなければ、Service Locatorという別のアンチパターンになってしまうので注意が必要です。(こちらは機会があれば別途書きます)

見るべき動画