メリットは?
- コンポーネント間の依存関係を取り除ける
- コードの変更を行いやすい
- unit test をしやすい
変更に弱いクラスの構成
Dog
とCat
クラスはどちらも、引数に与えられた数字を返すだけの関数number
を持っています
Animal
クラスではそれぞれのクラスを直接インスタンス化しています
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
クラスに変更しなければならなくなった場合
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))
この少量のコードに対してでも、変更箇所が多いです。
これが、規模が大きいものだとカオスなことになりかねないです
では、どう対応すれば良いか?
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
ではオブジェクトも受け取れてしまいます
そこで、型ヒントを使います
class Animal:
def __init__(self, animal_1: Cat, animal_2: Horse):
しかしこれではまた、animal_1
にはCat
クラス、animal_2
にはHorse
クラスしか受け取れない(厳密には、mypyなどサードパーティの型チェックが必要)ので汎用性がなくなります
では、どうすれば良いのか?
抽象クラスを使用します
抽象クラス・・・内部的には機能が存在しないクラス
抽象クラス自体は機能を持たないので、抽象クラスを継承したクラスが抽象メソッドをオーバーライドし処理を行えるように実装します
そうすることで、依存関係の解決ができます
実際にコードで確認してみます
抽象クラスの作成
以下が抽象クラスです
abstractAnimal
内のメソッドtotal_number
では何も処理を行っていないことが分かります
from abc import ABCMeta, abstractmethod
class AbstractAnimal(metaclass=ABCMeta):
@abstractmethod
def total_number(self, num_1: int, num_2: int):
pass
では、実際に使用してみます
先ほど作成した抽象クラスを使用してDIする
先程追加した抽象クラスAbstractAnimal
を継承させます
そして、Animal
クラスのコンストラクタでは型ヒントを追加します
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))
その結果、依存性の逆転という現象が起こります
依存性の逆転
これまで、クラス間同士で依存しあっていましたが、間に抽象クラスを噛ませることによって、具象クラスであるCatや,Horse
が抽象クラスに対して依存するようになリました
DIコンテナについて
以下、追記しました(2022/12/30)
DIコンテナとは、コンポーネント間の依存関係の管理を行うソフトウェアの事です
DIコンテナとして、injectorというライブラリを使用したものを例に取ります。
It automatically and transitively provides dependencies for you.
これを使用することで、依存関係を自動的かつ過渡的に管理します
実際に使用する
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):
・・・型ヒントは必須です
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
をする事で実際に依存関係の解決をし、インスタンスを作成します
参考