【Python】DI、DIコンテナについて

## メリットは？

* コンポーネント間の依存関係を取り除ける
    * コードの変更を行いやすい
    * unit test をしやすい

## 変更に弱いクラスの構成

`Dog`と`Cat`クラスはどちらも、引数に与えられた数字を返すだけの関数`number`を持っています

`Animal`クラスではそれぞれのクラスを直接インスタンス化しています

```sample.py
class Cat:
    def number(self, num: int) -> int:
        return num


class Dog:
    def number(self, num: int) -> int:
        return num


class Animal:
    animal_1 = Cat() # => ここで直接インスタンス化
    animal_2 = Dog() # => ここで直接インスタンス化

    def total_number(self, num_1: int, num_2: int) -> int:
        return self.animal_1.number(num_1) + self.animal_2.number(num_2)


animal = Animal()
print(animal.total_number(10, 20))
# > 30
```

しかし、これでは問題があってコードに変更に弱いです

## 先ほどのコードが仕様の変更などによって、修正する必要がある場合

`Dog`ではなく、`Horse`クラスに変更しなければならなくなった場合

```sample.py
class Cat:
    def number(self, num: int) -> int:
        return num


class Horse: # ※修正1
    def number(self, num: int) -> int:
        return num


class Animal:
    animal_1 = Cat()
    animal_2 = Horse() # ※修正2

    def total_number(self, num_1: int, num_2: int) -> int:
        return self.animal_1.number(num_1) + self.animal_2.number(num_2)


animal = Animal()
print(animal.total_number(10, 20))
```

この少量のコードに対してでも、変更箇所が多いです。
これが、規模が大きいものだとカオスなことになりかねないです

## では、どう対応すれば良いか？

```sample.py
class Cat:
    def number(self, num: int) -> int:
        return num


class Horse:
    def number(self, num: int) -> int:
        return num


class Animal:
    def __init__(self, animal_1, animal_2): # コンストラクタでインスタンスを受け取る
        self.animal_1 = animal_1
        self.animal_2 = animal_2

    def total_number(self, num_1: int, num_2: int) -> int:
        return self.animal_1.number(num_1) + self.animal_2.number(num_2)

# クラスの外でインスタンス化
cat = Cat()
horse = Horse()
animal = Animal(cat, horse)
print(animal.total_number(10, 20))
```

ここでの変更点は以下です
* コンストラクタでインスタンスを受け取る
* クラスの外でインスタンス化

しかしこれではまだ、コンストラクタの引数`animal_1, animal_2`ではオブジェクトも受け取れてしまいます

そこで、型ヒントを使います

```sample.py
class Animal:
    def __init__(self, animal_1: Cat, animal_2: Horse):
```

しかしこれではまた、`animal_1`には`Cat`クラス、`animal_2`には`Horse`クラスしか受け取れない（厳密には、mypyなどサードパーティの型チェックが必要）ので汎用性がなくなります

では、どうすれば良いのか?

## 抽象クラスを使用します

抽象クラス・・・内部的には機能が存在しないクラス

抽象クラス自体は機能を持たないので、抽象クラスを継承したクラスが抽象メソッドをオーバーライドし処理を行えるように実装します

そうすることで、依存関係の解決ができます

実際にコードで確認してみます

## 抽象クラスの作成

以下が抽象クラスです

`abstractAnimal`内のメソッド`total_number`では何も処理を行っていないことが分かります

```sample.py
from abc import ABCMeta, abstractmethod


class AbstractAnimal(metaclass=ABCMeta):
    @abstractmethod
    def total_number(self, num_1: int, num_2: int):
        pass
```

では、実際に使用してみます

## 先ほど作成した抽象クラスを使用してDIする

先程追加した抽象クラス`AbstractAnimal`を継承させます

そして、`Animal`クラスのコンストラクタでは型ヒントを追加します

```saple.py
from abc import ABCMeta, abstractmethod


class AbstractAnimal(metaclass=ABCMeta):
    def total_number(self, num_1: int, num_2: int):
        pass


class Cat(AbstractAnimal):
    def number(self, num: int) -> int:
        return num


class Horse(AbstractAnimal):
    def number(self, num: int) -> int:
        return num


class Animal:
    def __init__(self, animal_1: AbstractAnimal, animal_2: AbstractAnimal):
        self.animal_1 = animal_1
        self.animal_2 = animal_2

    def total_number(self, num_1: int, num_2: int) -> int:
        return self.animal_1.number(num_1) + self.animal_2.number(num_2)


cat = Cat()
horse = Horse()
animal = Animal(cat, horse)
print(animal.total_number(10, 20))

```

その結果、依存性の逆転という現象が起こります

## 依存性の逆転

![スクリーンショット 2022-09-12 16.37.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536974/b8f83681-ec44-e1c3-005a-229503643d2b.png)

これまで、クラス間同士で依存しあっていましたが、間に抽象クラスを噛ませることによって、具象クラスである`Catや,Horse`が抽象クラスに対して依存するようになリました

# DIコンテナについて

以下、追記しました（2022/12/30）

DIコンテナとは、**コンポーネント間の依存関係の管理を行うソフトウェアの事**です

DIコンテナとして、[injector](https://github.com/python-injector/injector)というライブラリを使用したものを例に取ります。

> It automatically and transitively provides dependencies for you.

これを使用することで、依存関係を自動的かつ過渡的に管理します

https://github.com/python-injector/injector

## 実際に使用する

```python
from injector import inject


class AbstractAnimal(metaclass=ABCMeta):
    def total_number(self, num_1: int, num_2: int):
        pass


class Animal:
    @inject
    def __init__(self, animal_1: AbstractAnimal, animal_2: AbstractAnimal):
        if not isinstance(animal_1, AbstractAnimal):
            raise Exception("animal_1 is not AbstractAnimal")
        if not isinstance(animal_2, AbstractAnimal):
            raise Exception("animal_2 is not AbstractAnimal")

        self.animal_1 = animal_1
        self.animal_2 = animal_2
```

`@inject`で、コンストラクタに対して依存先オブジェクトを注入するための宣言をデコレータでしています
`def __init__(self, animal_1: AbstractAnimal, animal_2: AbstractAnimal):`・・・型ヒントは必須です

```python
from injector import inject, Injector, Module

.
.
.

class AnimalDIModule(Module):
    def configure(self, binder):
        binder.bind(Cat, to=Cat)
        binder.bind(Horse, to=Horse)


if __name__ == '__main__':
    di = Injector(AnimalDIModule())
    animal = Animal(di.get(Cat), di.get(Horse))
    print(animal.total_number(10, 20))
```

続いて、`Module` クラスを継承した`AnimalDIModule` クラスを作成し、依存関係の設定をします

`di.get`をする事で実際に依存関係の解決をし、インスタンスを作成します


# 参考
https://speakerdeck.com/shimakaze_soft/python-de-dependency-injection-di-woyaruniha

https://github.com/python-injector/injector