備忘録としてDependency Injectionの基礎知識とPythonでの実装方法を整理します。
環境
- Python 3.8.10
DIとは?
 Dependency Injection の略です。具体的に言うと、クラスAで利用するインスタンスをクラスAの内部で生成するのではなく、抽象に依存させて外部から渡すことです。
サンプルコードを見てみましょう。
悪い例
class Cat():
    def __init__(self, name: str):
        self.name = name 
    
    def cry(self):
        print(f'{self.name} cry meow meow.')
class A():
    
    cat1 = Cat("ハチワレ")
    
    def action(self):
        self.cat1.cry()
if __name__ == "__main__":
    a = A()
    a.action()
Aクラスの内部でCatクラスインスタンスを生成し、cryメソッドを呼び出しています。
動作上は問題ありませんが、この実装だと猫の名前を変えるたびにAクラスを変更しなければなりません。名前が変わったというCatクラスの事情がAクラスに影響しています。
CatクラスとAクラスが密結合になっており、保守性が低い状態です。
そこで、以下ように変更します。
- Aクラスインスタンスに外部からCatクラスインスタンスを渡す
- AクラスではCatクラス(具象)ではなく、Animalクラス(抽象)に依存する。
変更後のコードがこちらです。
良い例
from abc import ABCMeta, abstractmethod
# 抽象クラス
class Animal(metaclass=ABCMeta):
    
    @abstractmethod
    def cry(self):
        pass
    
# 抽象クラスを実装した具象クラス
class Cat(Animal):
    def __init__(self, name: str):
        self.name = name 
    
    def cry(self):
        print(f'{self.name} cry meow meow.')
class A():
    
    # 抽象クラスに依存する
    def __init__(self, animal: Animal) -> None:
        self.animal: Animal = animal 
    
    def action(self):
        self.animal.cry()
if __name__ == "__main__":
    
    # 抽象型としてCatインスタンスを生成する
    cat: Animal = Cat("ハチワレ")
    a = A(cat)
    a.action()
Catクラスの変更がAクラスに影響しにくい設計になりました。
(Catクラスのプロパティが変わってもAクラスはそれを知らなくても問題ない)
DIのメリット
① クラス同士を疎結合に保ちやすい。
サンプルコードで確認した通り、DIを使うと依存先クラス(サンプルの場合はCatクラス)の変更が依存元クラスに影響しにくくなります。変更時の影響が小さくなるのでプログラムを修正しやすくなるのが嬉しいです。
② 単体テストをしやすい
「DIのメリットはテストがしやすくなる」とよく言われますがこれには2つの意味があると思われます。1つはクラス同士が疎結合になることでテスト観点が明確になるという意味です。もう1つは依存対象のオブジェクトを外部から渡すことができるため、テスト用のモックを利用できるという意味です。
テスタビリティ向上はDIを採用する目的として大きいでしょう。
DIのデメリット
依存対象が増えると辛い
サンプルコードではAクラスがAnimalクラスにのみ依存しているため気になりませんが、Aクラスが複数のクラスに依存する場合面倒なことになります。
依存先クラスのインスタンスを全て生成→ Aクラスのインスタンスを生成する時に引数に渡す という手順が必要になります。
実装コストが増える他、コードの可読性が下がります。
この問題を解決するためにDIコンテナやサービスロケーターという実装パターンが採用されることが多いです。DIコンテナについては別の記事で扱おうと思います。