【SOLID原則】新人エンジニアが教える「依存関係逆転の原則」

設計パターン

こんにちは。新人プログラマーの岩本です。

今回はSOLID原則の1つ「依存関係逆転の原則(Dependency Inversion Principle)」について、自分なりに調べたことをまとめます。

プログラムを書く上で「依存関係逆転」と聞くと、初めて聞く方は戸惑うかもしれませんが、実はとってもシンプルで、しかも非常に強力な概念です。ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。

実装環境

  • 言語…Swift 5.8.1
  • OS… macOS Ventura 13.1
  • アプリ… Xcode 14.3.1

依存関係逆転の原則とは?

依存関係逆転の原則とはwikiで以下のように説明されています。

  1. 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。
  2. 2.抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。
https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87

シンプルにまとめると「具象を名指しで指定するな!」ってことです。 ここでの具象とは構造体やクラスそのものです。

では何を指定すればいいのかというと、抽象インターフェースです。

依存関係逆転の原則を料理に例える

依存関係逆転の原則(以下DIP)を「🍛料理」に例えてみましょう。

まず具象的なレシピ本を考えてみます。カレーのページをめくり材料を確認すると、「メークイン」「晩生」「向陽二号」など品種で指定してあります。全く柔軟性がなく誰が作っても全く同じ味になります。また指定された品種を揃えるのに一苦労です。

それに対して、DIPに即した抽象的なレシピ本では、普通に「じゃがいも」「玉ねぎ」「にんじん」などが挙げられています。この抽象的な指定により、料理には柔軟性が生まれます。特定の品種に縛られず、誰でも手軽に作れ、それでいて個性的な味わいが楽しめるのです。

このように抽象化することでより柔軟性が生まれ、より疎結合な設計ができます。この例だと「じゃがいも」が抽象インターフェースで、「メークイン」が具象型を表しています。

まだ言葉だとピンとこないと思うので、早速コード例を紹介します。

DIPに違反したコード

まずはDIPに違反したコードを見てみましょう。

struct SampleAPI {
    func fetchData() {
        print("Fetch data...")
    }
}

struct ViewModel {
    let api: SampleAPI
    
    func fetchData() {
        api.fetchData()
    }
}

let viewModel = ViewModel(api: SampleAPI())
viewModel.fetchData()

この例ではViewModelがSampleAPI、つまり具象クラスを参照しています。

ではここでテスト用のAPIに繋ぎかえることを考えてみましょう。

// テスト用のAPI
struct TestSampleAPI {
    func fetchData() {
        print("TEST: Fetch data...")
    }
}

struct ViewModel {
    let api: TestSampleAPI // 型を変更する必要がある
    
    func fetchData() {
        api.fetchData()
    }
}

let viewModel = ViewModel(api: TestSampleAPI())
viewModel.fetchData()

Test用のAPIを使うためにViewModelのapiの型を変更する必要が出てしまいました。これでは本番用とテスト用を切り替えるときに面倒です。

そこでDIPを適応します。

DIPに従って書き直す

ここからは上のコードをDIPに従って書き直していきます。

繰り返しますが、DIPは「具象じゃなく抽象型を指定する」ことです。

まずAPI通信の抽象インターフェース APIProtocol を定義します。

protocol APIProtocol {
    func fetchData()
}

API通信をするクラスはこのAPIPorotocolを準拠するようにします。

// 本番用のAPIクラス
struct SampleAPI: APIProtocol {
    func fetchData() {
        print("Fetch data...")
    }
}

// テスト用のAPIクラス
struct TestSampleAPI: APIProtocol {
    func fetchData() {
        print("Test: Fetch data...")
    }
}

ViewModelのapiプロパティの型をAPIProtocolに変更します。

struct ViewModel {
    let api: APIProtocol // 抽象型を指定
  
    func fetchData() {
        api.fetchData()
    }
}

先ほどまでapiプロパティはSampleAPIやTestSampleAPIなど、具象型を指定していましたが、APIProtocolを使い抽象型を指定することで、どちらの型のAPIにも対応できるようになりました。

これが依存関係逆転の原則です。

依存関係逆転の原則のメリット

依存関係逆転の原則を適応することのメリットは、クラス間を疎結合にできることです。

疎結合にできれば、テストがしやすくなったり、変更がしやすくなったりと、開発を進めやすくなります。

もちろん疎結合にせずとも動くコードは作れます。

しかし柔軟性がないコードは、いずれ変更のコストが利益を上回ります。そうなればユーザーの声に沿ってアプリを改善するのが難しくなります。

柔軟性を失い変更ができなくなったアプリは、いずれ衰退していきます。いいアプリやソフトウェアを作るのに、コードの柔軟性は必須なのです。

なんで依存関係「逆転」なの?

ところで、なぜ依存関係「逆転」の原則というのでしょう?何がどう逆転しているのでしょう。

上記で提示したDIPに反しているコードのクラス図を見てみましょう。

ViewModelがSampleAPIまたはTestSampleAPIに依存している状態です。これにAPIProtocolを挟むことで、こうなります。

ViewModelとAPIProtocolをViewModelというまとまりとしてみると、矢印が反対向きになっていることがわかります。

このように矢印(依存関係)が反対になったので「依存関係が逆転する」と言います。

これが依存関係逆転の原則の名前の意味です。

まとめ

今回は「依存関係逆転の原則」を紹介しました。

実際業務では当たり前のようにこのテクニックが使われています。でも理解さえしてしまえば、意外とシンプルな概念です。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

一緒に見てほしい記事

依存関係逆転の原則と似たような概念として「DI」があります。
DIについても過去に解説しているので、ぜひ一緒にご覧ください。

参考にした記事

依存性の逆転のいちばんわかりやすい説明

👆タイトルの通り一番わかりやすかったです

「依存性逆転の原則」と「依存性の注入」を完全に理解した - Qiita
はじめに本記事は Swift/Kotlin愛好会 Advent Calendar 2021 の17日目の記事です。空いていたので埋めました。「依存性逆転の原則」と「依存性の注入」を完全に理解し…

👆より詳細に知りたい人におすすめ

依存関係逆転の原則(DIP) – 野生のプログラマZ

👆具体例がわかりやすい

コメント

タイトルとURLをコピーしました