swiftで 依存関係逆転の原則 を使ってテストしやすい設計にする

  • 21
    いいね
  • 1
    コメント

今開発しているプロダクトで、既存のMVCのコードをクリーンアーキテクチャ(+DDD風味)で置き換えているのですが、
依存関係逆転を使ってドメイン層をどこにも依存させないような作りにしています
(依存とは、参照している状態。ということにします)

脳内整理がてら、依存関係逆転にいたるまで順序立てて書いていきます

(「依存関係逆転の原則」という言葉を知らなくても読めると思います)

依存関係逆転の原則 を使う動機

  • コードをテストしやすくする
  • 変更に強くする

多少コストは増えますが、テストが圧倒的に書きやすくなるメリットがあります。
また最後まで見ればわかるのですが、コード量もそれほど変わらないので、慣れてしまえば気にならないコスト感かと思います。

フェーズ1 下位レイヤーに依存したコード

SearchService.swift
/**
  検索結果を返すクラス
 **/
public class SearchService {

    let repository: YahooRepository

    init() {
        repository = YahooRepository()
    }

    func getResult(completion: (([Any]) -> Void)) {
        completion(repository.getSearchResults())
    }
}
YahooRepository.swift
/**
 ヤフー検索からデータを引いてくるクラス
 **/
public class YahooRepository {
    public func getSearchResults() -> [Any] {
        return ["検索結果1", "検索結果2"] //適当
    }
}

SearchServiceのgetResultを使って、ヤフー検索の検索結果を取得するコード例です。
SearchServiceが、下位であるYahooRepositoryを参照しています。(依存している)

特に依存関係に気を使ったりしていなければ、最初はこういったコードになりそうです

これの問題点

YahooRepositoryに依存してしまっているため、SearchService単体でのテストができません。
また、たとえばYahooRepositoryのメソッド名や戻り値に変更が入った場合、
SearchServiceのコード修正が必要になります(変更に弱い)

これの改善案

Dependency Injectionを使って、外からYahooRepositoryオブジェクトを注入することで、
単体でのテストができるようにします

フェーズ2 Dependency Injection を使う

SearchService.swift
public class SearchService {

    private let repository: YahooRepository

    init(repository: YahooRepository) {
        self.repository = repository
    }

    func getResult(completion: (([Any]) -> Void)) {
        completion(repository.getSearchResults())
    }
}
YahooRepository
public class YahooRepository {
    public func getSearchResults() -> [Any] {
        return ["検索結果1", "検索結果2"]
    }
}

init時に外からYahooRepositoryオブジェクトを注入するようにしました。
YahooRepositoryのサブクラスを作って注入してやることで、repositoryをモックにできるので、
SearchServiceのユニットテストが書けます。

これの問題点

サブクラス化によるモックのため、親クラスの挙動に影響を受けてしまう可能性があります
たとえば、親クラスのinit内で何か特殊なことをしている、とか…
final だったりするとそもそもサブクラス化できないです

また、フェーズ1でもあった、変更に弱いという問題の解決が出来ていません

これの改善案

Interface(Protocol)を使って抽象に依存させることで改善できそうです
プロトコル指向というやつです

フェーズ3 プロトコル指向

SearchService.swift
public class SearchService {

    private let repository: YahooRepositoryInterface //抽象に依存

    init(repository: YahooRepositoryInterface) {
        self.repository = repository
    }

    func getResult(completion: (([Any]) -> Void)) {
        completion(repository.getSearchResults())
    }
}
YahooRepository.swift
// protocolを定義
public protocol YahooRepositoryInterface {
    func getSearchResults() -> [Any]
}

// 抽象に依存させる
public class YahooRepository: YahooRepositoryInterface {
    public func getSearchResults() -> [Any] {
        return ["検索結果1", "検索結果2"]
    }
}

YahooRepositoryInterface というprotocolを作り、
SearchService、YahooRepository両方共、このprotocolに依存させるようにしました。
これによって、YahooRepositoryInterface に適合させたモックを作って渡すだけで、
SearchServiceのユニットテストができるようになりました

たとえば以下のようにです

RepositoryMock.swift
class RepositoryMock: YahooRepositoryInterface {
    func getSearchResults() -> [Any] {
        return ["mock dayo"]
    }
}

これの問題点

SearchServiceは検索を扱うサービスクラスです
しかし、YahooRepositoryはYahoo(外部)に依存しています
急にYahooの仕様変更があってもおかしくありません
そうなると、それに合わせてYahooRepositoryInterfaceも変更しなくてはならず、
YahooRepositoryInterfaceに依存しているSearchServiceもコード修正が必要になってしまいます

つまり、アンコントローラブルなYahooRepositoryがInterfaceを定義しているのが問題です

これの改善案

依存関係を逆転させる

フェーズ4 依存関係逆転の原則を使う

SearchService.swift
// protocolを定義
public protocol SearchServiceRepositoryInterface {
    func get() -> [Any]
}

public class SearchService {

    private let repository: SearchServiceRepositoryInterface //抽象に依存

    init(repository: SearchServiceRepositoryInterface) {
        self.repository = repository
    }

    func getResult(completion: (([Any]) -> Void)) {
        completion(repository.get())
    }
}
YahooRepository.swift
public class YahooRepository: SearchServiceRepositoryInterface {
    public func get() -> [Any] {
        return ["ヤフー検索結果1", "ヤフー検索結果2"]
    }
}

SearchServiceを主役に据えて、SearchServiceRepositoryInterfaceを用意しました。
YahooRepositoryはこのprotocolに依存させるような作りにしました。

SearchServiceとしては、検索結果をくれさえすれば、その裏側がどうなっていようが知ったことはないので
シンプルに「get()」というメソッド名にしています。

このようにしておくと、仮にYahooではなくGoogleの検索結果が使いたくなったとしても、以下のように実装するだけですぐに置き換えられます

GoogleRepository.swift
public class GoogleRepository: SearchServiceRepositoryInterface {
    public func get() -> [Any] {
        return ["グーグル検索結果1", "グーグル検索結果2"]
    }
}

外部に依存してしまう実装はどうしても急な変更が入りやすいので、このように上位レイヤーでprotocolを定義し、
下位レイヤーが上位レイヤーのprotocolに依存するようにしておくと、変更に強い実装にできます

以上でテストしやすく、変更に強いコードになりました

余談 クリーンアーキテクチャ

クリーンアーキテクチャは、このテクニックを使って
変更の起こりにくいものから順に円の内側から配置していき、
APIやViewと言った変更が入りやすい部分を「外界」と表現し、最も外側に配置
そして内側へ依存させるようにするのがミソです