依存性逆転の原則(Dependency Inversion Principle)とは Robert C. Martin 氏が挙げた5つのソフトフェアの設計指針 SOLID の一つです。

SOLID

・ Single Responsibility Principle (単一責任の原則)
・ Open-Closed Principle (オープン・クローズドの原則)
・ Liskov Substitution Principle (リスコフの置換原則)
・ Interface Segregation Principle (インタフェース分離の原則)
・ Dependency Inversion Principle (依存性逆転の原則)

今回はその SOLID の D である依存生の原則について文面、イラスト、実際のソースコードの順で説明していき、徐々に解像度を高めていくような形で解説していきます。

依存性逆転の原則とは「高レベルのモジュールは低レベルのモジュールに依存すべきではなく、両者とも抽象に依存すべき」という考え方です。
具体例を入れてもう少し噛み砕いていうと、 「PC などのマウスやキーボードを使う側である高レベルのモジュール」は「マウスやキーボードなどの使われる側である低レベルのモジュール」に依存してはいけないということになります。依存というのは使われる側の何かしらの変更により、使う側も何かしらの変更を必要とされる可能性がある状態のことを指します。

それではなぜ使われる側の変更により、使う側を変更することがダメなのでしょうか?

それは柔軟性と再利用性が損なわれるためです。
それらが損なわる理由としては、PC が周辺機器のデバイスに依存している世界を想像してもらうと分かりやすいと思います。
その世界だと新しいキーボードやマウスなどが販売されるたびに依存している PC の方も修正が必要になります。またそれらのデバイスを販売している企業は世界中にいくつもあり、その企業ごとに PC 側も対応する必要が出てきます。
以上のような対応をいちいちしていてはとてもキリがありませんし、仮に対応したとしてもほぼ毎日 PC のアプデや買い替えが必要になり、PC 側のどんなデバイスも使用できるような柔軟性や、別のキーボードでも使用可能になるような再利用性が著しく欠如した状態になります。

以上のような問題を解決するために、使う側と使われる側はある一定のルールを定めた規格に依存すること(抽象に依存する)で、使う側が使われる側の影響を受けないようにするというのが依存性逆転の原則になります。

以下の画像は PC がキーボードに依存していることを表している画像になります。
これだけ見ると特に問題ないような気もしますが、次の画像を見たらどうでしょう?!

以下の画像は PC が ABC それぞれのキーボードに依存している画像です。
この状態だと新しい種類のキーボードが増えるたびに専用の差し込み口や処理を用意しないといけなくなります。
それではこの問題を解決するために依存性逆転の原則に則り、抽象に依存する形へ変更してみましょう。

使う側である PC と使われる側であるキーボードが「接続デバイスの規格」という抽象に依存する形へ修正いたしました。
これで PC はキーボードの変更や種類を気にすることなく使用することができます。

PC が直接キーボどに依存してる場合

まずは PC がキーボードに依存している状態をコードで表現したいと思います。

キーボードクラスが抽象クラスに依存していないため、PC クラス内ではキーボードの数だけ、キーボードクラスのインスタンス変数と接続関数、入力受取関数を持つことになってしまっています。
そのため以下の処理はデバイスが増えるたびに PC クラスの処理を変更する必要があるため、柔軟性が欠如し、重複処理を生無用な形になっています。また特定のキーボードインスタンスに依存しているため、柔軟なテストが難しくなります。モックオブジェクトやテスト用のスタブを簡単に作成できない形になってしまっています。

class KeyboardA {
    func inputKey() -> String {
        return "A"
    }
}

class KeyboardB {
    func inputKey() -> String {
        return "B"
    }
}

class PC {
    private var keyboardA: KeyboardA?
    private var keyboardB: KeyboardB?

    // PCインスタンス生成時にはキーボードが接続されていない
    init() {}

    // KeyboardAを接続するためのメソッド
    func connectKeyboardA(_ keyboard: KeyboardA) {
        self.keyboardA = keyboard
    }

    // KeyboardBを接続するためのメソッド
    func connectKeyboardB(_ keyboard: KeyboardB) {
        self.keyboardB = keyboard
    }

    // KeyboardAから入力を受け取るメソッド
    func receiveInputFromKeyboardA() {
        guard let keyboardA = keyboardA else {
            print("No KeyboardA connected.")
            return
        }
        
        let keyPressed = keyboardA.inputKey()
        print("Received input '\(keyPressed)' from Keyboard A")
        processInput(keyPressed: keyPressed)
    }

    // KeyboardBから入力を受け取るメソッド
    func receiveInputFromKeyboardB() {
        guard let keyboardB = keyboardB else {
            print("No KeyboardB connected.")
            return
        }
        
        let keyPressed = keyboardB.inputKey()
        print("Received input '\(keyPressed)' from Keyboard B")
        processInput(keyPressed: keyPressed)
    }

    private func processInput(keyPressed: String) {
        switch keyPressed {
        case "A":
            print("Processing action for key A")
        case "B":
            print("Processing action for key B")
        default:
            print("Unknown key pressed")
        }
    }
}

// 使用例
let pc = PC()  // PCインスタンス生成時にはキーボードが接続されていない

let keyboardA = KeyboardA()
pc.connectKeyboardA(keyboardA)  // PCにキーボードAを接続
pc.receiveInputFromKeyboardA()
// Output: 
// Received input 'A' from Keyboard A
// Processing action for key A

let keyboardB = KeyboardB()
pc.connectKeyboardB(keyboardB)  // PCにキーボードBを接続
pc.receiveInputFromKeyboardB()
// Output: 
// Received input 'B' from Keyboard B
// Processing action for key B

依存性が逆転している場合

InputDevice という抽象クラスにキーボードのクラスが依存して、それを PC が使うことで、依存性を逆転させました。
そのため PC クラス内には InputDevice 型のインスタンスが一つだけが定義され、接続関数、入力受取関数も一つになり重複もなくなりました。

protocol InputDevice {
    func inputKey() -> String
    func deviceType() -> String
}

class KeyboardA: InputDevice {
    func inputKey() -> String {
        return "A"
    }
    
    func deviceType() -> String {
        return "Keyboard A"
    }
}

class KeyboardB: InputDevice {
    func inputKey() -> String {
        return "B"
    }
    
    func deviceType() -> String {
        return "Keyboard B"
    }
}

class PC {
    private var inputDevice: InputDevice?

    // PCクラスのインスタンス生成時にはキーボードが接続されていない
    init() {}

    // キーボードを後から接続するためのメソッド
    func connectKeyboard(_ keyboard: InputDevice) {
        self.inputDevice = keyboard
    }
    
    func receiveInput() {
        guard let inputDevice = inputDevice else {
            print("No keyboard connected.")
            return
        }
        
        let keyPressed = inputDevice.inputKey()
        let device = inputDevice.deviceType()
        
        print("Received input '\(keyPressed)' from \(device)")
        processInput(keyPressed: keyPressed)
    }
    
    private func processInput(keyPressed: String) {
        switch keyPressed {
        case "A":
            print("Processing action for key A")
        case "B":
            print("Processing action for key B")
        default:
            print("Unknown key pressed")
        }
    }
}

// 使用例
let pc = PC()  // PCインスタンス生成時にはキーボードが接続されていない

let keyboardA = KeyboardA()
pc.connectKeyboard(keyboardA)  // PCにキーボードAを接続
pc.receiveInput()
// Output: 
// Received input 'A' from Keyboard A
// Processing action for key A

let keyboardB = KeyboardB()
pc.connectKeyboard(keyboardB)  // キーボードBに切り替え
pc.receiveInput()
// Output: 
// Received input 'B' from Keyboard B
// Processing action for key B

pc.connectKeyboard(KeyboardB())
pc.receiveInput()

また抽象クラスを用意して柔軟性を向上させえたことで、以下のような Mock クラスを作ってテストをすることも容易になりました。

class MockKeyboard: InputDevice {
    private let mockKey: String
    private let mockDeviceType: String

    init(key: String, deviceType: String) {
        self.mockKey = key
        self.mockDeviceType = deviceType
    }

    func inputKey() -> String {
        return mockKey
    }

    func deviceType() -> String {
        return mockDeviceType
    }
}

// テストの例
func testPCWithMockKeyboard() {
    let pc = PC()

    let mockKeyboardA = MockKeyboard(key: "A", deviceType: "Mock Keyboard A")
    pc.connectKeyboard(mockKeyboardA)
    pc.receiveInput()
    // Output:
    // Received input 'A' from Mock Keyboard A
    // Processing action for key A

    let mockKeyboardB = MockKeyboard(key: "B", deviceType: "Mock Keyboard B")
    pc.connectKeyboard(mockKeyboardB)
    pc.receiveInput()
    // Output:
    // Received input 'B' from Mock Keyboard B
    // Processing action for key B

    let mockKeyboardUnknown = MockKeyboard(key: "X", deviceType: "Unknown Keyboard")
    pc.connectKeyboard(mockKeyboardUnknown)
    pc.receiveInput()
    // Output:
    // Received input 'X' from Unknown Keyboard
    // Unknown key pressed
}

// テストの実行
testPCWithMockKeyboard()