LoginSignup
60
55

More than 3 years have passed since last update.

テストのためのDI(Dependency Injection)パターンを考える

Last updated at Posted at 2020-01-13

k2moons@BlueEventHorizonです。昨年から気になっていたテストとDIについてやっと書くことができました。お正月休みでは終わらず成人式を含んだ3連休も最終日まで掛かってしまいました。😭

はじめに

DI(Dependency Injection)はよく使われる手法ですが、テストの文脈で語られることが多いようです。
そしてテストの実現容易性のためにDI(Dependency Injection)を用いることの有用性は疑う余地がないでしょう。

しかし、実際にDIを用いる場合、どのような視点のDependencyであって、それがどのような理由でInjectionされるとのかという点でいくつかのバリエーションがあり、それについて考察することは有意義ではないでしょうか。

この記事では、特にテストを行うためのDIと言う文脈で、日頃考えていることを書き連ねていきます。少しでもお役に立てれば幸いです。

記事内の使用言語

使用する言語は、

  • UML
  • Swift

です。
最後にPlantUMLを一部のせました。

依存が密結合なDI

まず密結合、疎結合な依存1、というところから始めたいと思います。

DIとは依存(Dependency)をhas-aの関係で対象クラスに持たせる(注入する:Injection)ことです。

ここで言う依存とは注入可能な依存であって、すなわち下で述べる<<interface>>Protocolの形式を持つものです。

密結合な依存とはなんなのか。
まずシンプルな例をUML:クラス図を用いて説明します。

UML:クラス図

下の例では、Speakableという抽象にPersonが依存しています。
Speakableの<<interface>>とは抽象のことで、Swiftの場合はProtocolと考えて差し支えないでしょう。2
SpeakPolitely、SpeakRudelyはSpeakableを実装したオブジェクトで、SpeakableとしてPersonクラスに注入されます。
speak()は、Personクラスのnegotiate()から呼び出されて利用されますので、negotiate()を実行した時の挙動をSpeakPolitely、SpeakRudelyの注入によって選択できる(機能を差し替えられる)ことになります。

スクリーンショット 2020-01-11 14.58.56.png

PersonクラスとSpeakableの間は、ひし形の塗りつぶしであった記号と線で結ばれています。
これはUMLにおいてcompositionと呼ばれるものです。

compositionは、SpeakableはPersonの一部であり、さらにライフサイクルも同一である
ということを意味しています。3

そして、この関係が密結合しているDependencyだと考えています。

realizationとは抽象の実現を意味しています。ようするにProtocolを実装したクラスを作ったということですね。このケースでは、Speakableという抽象をSpeakPolitely、SpeakRudelyクラスとして実現していることになります。

次に実際にSwiftコードに落とし込んだものを見てみます。

Swiftコード

自分はDIを行う時のDependency、すなわちProtocolの記述をどこで行うのか、が大変重要だと思っています。
この場所によって密結合なのか疎結合なのか、設計意図を推し量ることができると思っているからです。
密結合の場合、同一のファイル内か、ファイルを分けたとしても非常に近い位置に配置することが一般的だと思います。
下記の例では、クラス図に書いたようにPersonクラスとSpeakableが密結合のため、同一のファイル内の配置しています。


import Foundation

protocol Speakable {
    func speak()
}

class Person {

    private var dependency: Speakable
    
    init(_ dependency: Speakable) {
        self.dependency = dependency
    }
    
    func negotiate() {
        dependency.speak()
    }
}

抽象の実現として、Speakableを継承したクラスを定義します。


class SpeakPolitely: Speakable {
    func speak() {
        print("こちらを減らせば、お安くなりませんか")
    }
}

class SpeakRudely: Speakable {
    func speak() {
        print("高いな〜")
    }
}

以下で依存を注入していきます。


let politePerson = Person(SpeakPolitely())
politePerson.negotiate()  // こちらを減らせば、お安くなりませんか

let rudePerson = Person(SpeakRudely())
rudePerson.negotiate()    // 高いな〜

ここで登場したクラスやプロトコルやプロトコルを実現したクラスは、基本的にPersonクラスに(たとえDIを用いられていても)密結合しています。(他のクラスにとって意味がない、と言い換えることもできるかも知れません)

依存が疎結合なDI

疎結合な依存とはどんなものでしょうか。
疎結合なクラス群というのは想像しやすいと思いますが、疎結合であると言うことはそもそもクラス間で関連が存在していると言うことです。
ですので、それこそが依存が疎結合な状態と考えて良いと思います。
しかしながらDIであると言うことは、単純に関連があるだけではありません。関連の先は、クラスではなく抽象である必要があります。4

下に示すクラス図では、在庫一覧クラスが、商品情報リストを得るために、対象となるユーザの会員番号、店舗の情報をそれぞれ異なる抽象(product, user, shop)から取得しています。

UML:クラス図

スクリーンショット 2020-01-13 14.37.55.png

在庫一覧クラスと各管理クラス抽象の間に引かれた線は、PersonクラスとSpeakableの間では、ひし形の塗りつぶしであった記号と線(composition)でしたが、こちらではただの実線のみです。
この線のことをUMLではassociationと呼び、各クラスとは密結合せず5、単純なメソッド呼び出しなどで関連していることを意味しています。当然ライフサイクルも異なります。

さらに各管理クラスはここには記述がない他のクラスのために情報の追加や、削除、更新機能を有しています。

DIの目的の違い

上記の2つの例は、密結合、疎結合という違いがありましたが、実は目的も異なっています。
それは依存を注入される側の機能を変更したいかどうかということです。依存が密結合なDIの例では、もともとの目的が機能の切り替えのためのDIを採用したということになりますので必須です。切り替えたくないのなら、そもそも分離することは無意味です。

依存が疎結合なDIの例では、実は機能を切り替えることはありません。もちろんそのような要件があってDIを用いることが必要な場合もあるでしょう。しかし、今回提示した例では現在のところDIが必要なようには見えません。

実は別に目的があるといことです。下のクラス図を見てください。

スクリーンショット 2020-01-13 14.39.08.png

この新しいクラス図では、各管理クラスの代わりにテスト用スタブに切り替えることができるようになっています。つまりDIの目的はテストであるということです。

何が問題なのか

みなさんは、機能要件を実現するための設計と、テストを目的にした設計を区別せずに、たまたま同じ設計でできるからと言う理由で、DIしてしまうのに違和感がありませんか?
私は後者の目的においてはいくつかの問題があると考えています。

不要な機能の注入

まず問題点としてあげられるのは、不要な機能が注入されているということです。
在庫一覧クラスは、注入されたクラスの一部の機能しか必要としていません。
在庫一覧で必要なのは各管理クラスから情報が取得できれば良いのであって、追加、更新、削除のメソッドは必要ありません。

もしテストしないのならばこれらのメソッドは呼び出せるけど使わないだけで終わってしまいますが、テストを考えるとそうはいきません。テストのために不必要なメソッドまで(たとえ中身が空であっても)実装する必要があります。これはクラスの規模が大きくなってくると無視できないほどの負荷になるでしょう。

目的と注入されたオブジェクトのメソッド名が一致しない(ことがある)

またテスト以外でも問題はあります。お互いに独立して外部にメソッドを公開しているために、呼び出す側の目的と、実際に呼びださなければならないメソッド名称の乖離が発生します。このことは、ソースコードを読解する上ではすくなからぬ障害になると考えます。

  • 呼び出す側の目的: 会員番号を利用したい
  • 呼び出すメソッド名: fetchData()

例があまり良くないですが、言わんとすることは分かっていただけるのではないかと思います。
自分自身のメソッドであれば、適切な名前を付けることも可能ですが、独立した外部のクラスのメソッド名はまったく別の理由で名付けられていることもあります。特に設計者・実装者が他人の場合は(ましてやスキルレベルや開発会社が異なっていたりすると)頻繁に遭遇するかもしれません。

依存が散逸する

依存した外部のプロトコルによる実装を直接呼び出すことで、依存が散逸します。
在庫一覧クラスの例で言えば、依存は変数product, user, shopに格納され在庫一覧クラスの各所で呼びだされたりします。
依存がproduct1つだけであればproductを検索すれば良いだけですが、依存が複数になりさらに上記のメソッド名が一致しない問題と相まってソースコードの読解はより難しくなっていくでしょう。

改善案

UML:クラス図

依存が密結合なDIと同様に在庫一覧はcompositionの関係にある在庫一覧DependencyProtocolを持ちます。この在庫一覧DependencyProtocolには、在庫一覧が依存するメソッドしか記述されていません。
このようにすることで、不要な機能の注入を避けることができます。

また目的と注入されたオブジェクトのメソッド名が一致しない問題も回避することができます。在庫一覧DependencyProtocolのメソッド名は在庫一覧クラスが自由に名付けられるからです。例えばこの例では、商品一覧取得であったものを在庫一覧取得などのように付け替えています。

依存が散逸する問題も在庫一覧DependencyProtocolに依存が集められたことで、在庫一覧が外部に依存するすべての項目が一覧できるようになります。

スクリーンショット 2020-01-13 14.40.11.png

Swiftコード

Swiftでは下記のように書けると思います。


protocol StockItemsDependencyProtocol {
    func getStockItems(member: Member, shop: Shop) -> [Product]
    func getMember() -> Member
    func getShopInfo() -> Shop
}

class StockItems {
    private var dependency: StockItemsDependencyProtocol = StockItemsDependency()
    
    init(_ dependency: StockItemsDependencyProtocol) {
        self.dependency = dependency
    }
    func showList() {
        let list = dependency.getStockItems(member: dependency.getMember(), shop: dependency.getShopInfo())
        // 在庫一覧描画
    }
}

class StockItemsDependency: StockItemsDependencyProtocol {
    func getStockItems(member: Member, shop: Shop) -> [Product] {
        return ProductManager().getProducts(member: member, shop: shop)
    }
    func getMember() -> Member {
        return MemberManager().getMember()
    }
    func getShopInfo() -> Shop {
        return ShopManager().getShop()
    }
}

疎結合の外部クラスの定義です。


class ProductManager {
    func addProduct(product: String) { }
    func getProducts(member: Member, shop: Shop) -> [Product] {
        return [Product]()
    }
    func updateProduct() { }
    func deleteProduct() { }
}

class MemberManager {
    func addMember(member: Member) { }
    func getMember() -> Member {
        return Member()
    }
    func updateMember() { }
    func deleteMember() { }
}

class ShopManager {
    func addShop(shop: Shop) { }
    func getShop() -> Shop {
        return Shop()
    }
    func updateShop() { }
    func deleteShop() { }
}
}

テストのためのDI

テストでは、下のクラス図のように「在庫一覧DependencyProtocol」の実現としてテストスタブを追加します。

UML:クラス図

スクリーンショット 2020-01-13 14.40.53.png

この手法にも何点かの問題があります。
その一つが、不要な機能の注入です。あれっと思われると思いますが、この場合の不要なはテストにとってと言うことです。
今回の例はシンプルすぎて発生しませんが、例えば在庫一覧クラスの特定のメソッドをテストするときに、在庫一覧DependencyProtocolの特定のメソッドしか必要がない場合、他の全てのメソッドを実装することになると面倒です。このような問題を回避するための手法として下記のような書き方を考えました。

Swiftコード

StockItemsDependencyProtocolのprotocol extensionを作成しますが、基本的にメソッドを(プロパティがある場合はプロパティも)実装しないで使用するとassertします。ただし、呼びださなければ当然assertしません。


extension StockItemsDependencyProtocol {
    func getStockItems(member: Member, shop: Shop) -> [Product] {
        assert(false)
        return  [Product]()
    }
    func getMember() -> Member {
        assert(false)
        return Member()
    }
    func getShopInfo() -> Shop {
        assert(false)
        return Shop()
    }
}

このようなprotocol extensionを持った状態で、StockItemsのgetMember()に依存する機能だけをテストしようとすると下記のように書くことができます。


class StockItemsTests: XCTestCase {
    // ここでDependencyを実装し、
    func testGetMemberNumber() {
         class StockItemsDependencyTestStub: StockItemsDependencyProtocol {
             func getMember() -> Member {
                 return Member(number: 27809, type: "paied")
             }
         }
         
        let stockItems = StockItems()
        stockItems.dependency = StockItemsDependencyTestStub() // ここで注入し、
        let menberNumber = stockItems.getMemberNumber() // テストする
        XCTAssertEqual(menberNumber, "27809")
    }
}

テストに必要なgetMember()だけを実装して、StockItemsクラスのgetMemberNumber()をテストします。
不要なメソッド等の実装が必要なく、テストに必要なすべてが集約されて記述されているので、実装者以外のエンジニアが見ても分かりやすくできていると思います。

またもし内部の実装が変更などされ、StockItemsクラスがgetMemberNumber()内でgetMember()以外を使うようになった場合、protocol extensionの記述にあるようにassertするのでテストが不完全であることがすぐに分かります。

プロダクションコードを汚していないのも、プロダクションコードに必要なコードがやはりStockItemsクラスに集約して記述できることもとても大きなポイントだと思います。

このようにプロダクションコードと、テストコードを分離し、記述するコードを極力減らすことでテストを書く負担が少しでも減少すればと考えています。

このさき

ここまで書きたいことの90%は終わっています。
しかし、この先判断がつきにくい問題があるので書きておきます。
まずはSwiftコードをご覧ください。


protocol StockItemsDependencyProtocol {
    func getStockItems(memberNumber: Int, shop: Shop) -> [Product]
    func getMemberNumber() -> Int
    func getShopInfo() -> Shop
}

class StockItems {
    private var dependency: StockItemsDependencyProtocol = StockItemsDependency()
    
    init(_ dependency: StockItemsDependencyProtocol) {
        self.dependency = dependency
    }
    func showList() {
        let list = dependency.getStockItems(memberNumber: dependency.getMemberNumber(), shop: dependency.getShopInfo())
        // 在庫一覧描画
    }
}

class StockItemsDependency: StockItemsDependencyProtocol {
    func getStockItems(memberNumber: Int, shop: Shop) -> [Product] {
        return ProductManager().getProducts(memberNumber: memberNumber, shop: shop)
    }
    func getMemberNumber() -> Int {
        // グルーロジック
        return MemberManager().getMember().number
    }
    func getShopInfo() -> Shop {
        return ShopManager().getShop()
    }
}

StockItemsDependencyProtocolが期待する依存を、外部のクラスが直接提供できない場合があります。
できないと言うより、StockItemsDependencyにロジックを置くことで更にプロダクションコードが明瞭になることがあります。
上記の例では、StockItemsは、メンバー番号を期待しており、MemberManagerは、Memberクラスを返すことしかしません。このような場合、StockItemsDependencyには、グルーロジック、つまり糊付けロジックが存在することになります。

何が問題かというと、テストにおいて注入されるのはStockItemsDependencyProtocolであるために、テスト時にはグルーロジックごと抹消されてこの部分がテストされない、ということです。カバレッジを100%にしたい場合は採用できませんが、効率を優先するならばMemberクラスを生成するより、Int値を返すスタブを作成する方が遥かに楽です。
ここのカバレッジか効率化はプロジェウトの性格にもよると思いますが、非常に短期間の開発を要求される場合は、一考に値すると思います。

おまけ

PlantUML

最後に、最後のクラス図だけ載せてきます。
iOSの開発ではUMLを持ち出すことがあまりないのですが、本記事のようなものを提示する場合は便利かも知れません。
気になる方はこちらもどうぞ
設計ドキュメント(UML)をPlantUMLで書いてみる


@startuml

class 在庫一覧
在庫一覧 : - dependency: 在庫一覧DependencyProtocol
在庫一覧 : + 在庫一覧描画()

interface 在庫一覧DependencyProtocol << interface >>
在庫一覧DependencyProtocol : + 在庫一覧取得(会員,店舗)
在庫一覧DependencyProtocol : + 会員取得()
在庫一覧DependencyProtocol : + 店舗取得()

在庫一覧 "1" *-- "1" 在庫一覧DependencyProtocol : composition
在庫一覧DependencyProtocol -[hidden]ri- 在庫一覧

class 在庫一覧Dependency
在庫一覧Dependency :  + 在庫一覧取得(会員,店舗)
在庫一覧Dependency :  + 会員取得()
在庫一覧Dependency :  + 店舗取得()

在庫一覧DependencyProtocol <|.. 在庫一覧Dependency : realization

class 在庫一覧Dependencyテストスタブ
在庫一覧Dependencyテストスタブ :  + 在庫一覧取得(会員,店舗)
在庫一覧Dependencyテストスタブ :  + 会員取得()
在庫一覧Dependencyテストスタブ :  + 店舗取得()

在庫一覧DependencyProtocol <|.. 在庫一覧Dependencyテストスタブ : realization

在庫一覧Dependency  "1" -- "1" 商品管理DB : association
在庫一覧Dependency  "1" -- "1" 会員管理DB : association
在庫一覧Dependency  "1" -- "1" 店舗管理 : association

note bottom of 在庫一覧Dependencyテストスタブ : "追加"


class 商品管理DB
商品管理DB : + 商品追加()
商品管理DB : + 商品一覧取得(会員,店舗)
商品管理DB : + 商品取得()
商品管理DB : + 商品更新()
商品管理DB : + 商品削除()


class 会員管理DB
会員管理DB : + 会員追加()
会員管理DB : + 会員取得()
会員管理DB : + 会員更新()
会員管理DB : + 会員削除()


class 店舗管理
店舗管理 : + 店舗追加()
店舗管理 : + 店舗取得()
店舗管理 : + 店舗更新()
店舗管理 : + 店舗削除()

@enduml
  1. ちなみにこの言い方はオリジナルなものなので、もし一般的な概念として呼称があるようならば教えていただけると嬉しいです。

  2. Swiftには抽象クラス(abstract)という概念がないと思いますが、Javaでは抽象クラスの注入もありえます。

  3. 【コードで分かるUMLシリーズ】クラス図の書きかた(集約とコンポジションの意味の違い)

  4. 厳密には抽象である必要はないかも知れませんが、その場合はDIの意味がなさそうです。

  5. あいまいな表現になっていますが、密結合とcompositionは別の概念です

60
55
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
55