この記事を書いた理由
Unityを触っていた時にこの原則について知ったんですけど、最初見たときは何言ってるか分かんなかったんですよ。でも理解できないのは気に食わなかったので、Pythonで実際にこの原則を適用して開発してみたんです。
それである程度理解することもできて、メリットデメリットも感じられたんです。共有するいい機会だと考えて、記事を書くことにしてみました。
依存性逆転原則って何?
オブジェクト指向プログラミングの原則の一つ。SOLID原則のD(Dependency Inversion Principle: DIP)に当てはまる。これを適用することで開発中のソフトウェアの拡張性や保守性が高くなる。
ようするにこの通りに開発していけば、チームでのソフトウェア開発がとてもやりやすくなる原則の一つであるということ。
SOLID原則とは?
オブジェクト指向プログラミングにおける、5つのソフトウェア開発原則。Robert C.Martin が提唱したもので、これに従うことで開発するソフトウェアの拡張性や保守性を高めることができる。
くわしくは以下の記事で
イラストで理解するSOLID原則
どういった原則なのか
Robert C.Martin が提唱した原則の訳は以下の通り。
・上位モジュールは、下位モジュールに依存してはならない。どちらも抽象に依存すべきである。
・抽象化は詳細に依存してはならない。詳細が抽象化に依存すべきだ。
???
これだけだと何言ってるか分かりませんね。(分かった人はすごい人)
理解をするために、実際に依存性逆転がどのように適用されるのか見ていきましょう。
適用の仕方
例えば次のようなPythonのプログラムがあるとします。
class A:
def __init__(self) -> None:
self._a = 10;
self._b = 20;
def get_2a(self) -> int:
return 2*self._a;
def get_3b(self) -> int:
return 3*self._b
from A import A
class B:
def __init__(self) -> None:
self.k = 10
self.a = A()
def print_k1(self):
print(self.k + self.a.get_2a())
if __name__ == "__main__":
b = B()
b.printk1()
この二つの内、クラスBはAに用意されている関数がないと機能しないようになっています。こういった状態をBがAに依存していると言います。
依存関係を矢印で表すと以下のようになります。
B -> A
依存性逆転の原則ではこういった依存関係は許可されていません。BがAの上位モジュールになり、Aという具体的な下位モジュールに依存してしまっているからです。
そこで、Aのクラスの型となる、AのインターフェースAIを用意します。
from abc import ABCMeta,abstractclassmethod
class AI(metaclass = ABCMeta):
@abstractclassmethod
def get_2a(self) -> int:
pass
インターフェースは、いわば継承させたクラスに書いてあるメソッドの実装を強制させるものです。AがAIを継承して、中にget_2aなどのメソッドの記載がないとエラーを吐くようになっています。
つまりAIをAに継承させると、AIがAの抽象的な型になってくれるわけです。
これをB.py内に記載し、Aに継承させます。そして、B内の a の中身を外部から入れるようにします。
from B import AI
class A(AI):
def __init__(self) -> None:
self._a = 10;
self._b = 20;
def get_2a(self) -> int:
return 2*self._a;
def get_3b(self) -> int:
return 3*self._b
from abc import ABCMeta,abstractclassmethod
class AI(metaclass = ABCMeta):
@abstractclassmethod
def get_2a(self) -> int:
pass
class B:
def __init__(self, a: AI) -> None:
self.k = 10
self.a: AI = a
def print_k1(self):
print(self.k + self.a.get_2a())
if __name__ == "__main__":
from A import A
b = B(A())
b.printk1()
こうなると、AはAIを継承するためAIに、BはAI型の変数 a を用いている(ソース上では)ことになるためAIにと、どちらもAIに依存していることになります。
B -> AI <- A
ここでAIはBの一部と捉えることができます。何故ならAIはBが用いるための関数しか記載しておらず、完全にAをB内で扱うための部品となっているためです。
そこでAI、BをB'としてまとめると、依存関係は以下のようになります。
B' <- A
これにより、最初の関係 B->A から依存関係が逆転し、依存性逆転の原則が適用されたことになります。
どうしてこんなことをする必要があるのか
例として挙げた二つのクラスの内、AをKさんが、BをSさんが開発しているとします。このとき、お互いに開発しているクラスの中身は知らず、自分が担当するクラスしか変更できないものとします。
ここで、Kさんが get_2a の名前を get_a2 にしてしまった場合を考えましょう。
get_2a を get_a2 にしてしまった場合
適用前
class A:
def __init__(self) -> None:
self._a = 10;
self._b = 20;
def get_a2(self) -> int:
return 2*self._a
def get_3b(self) -> int:
return 3*self._b
from A import A
class B:
def __init__(self) -> None:
self.k = 10
self.a = A()
def print_k1(self):
print(self.k + self.a.get_2a())
if __name__ == "__main__":
b = B()
b.printk1()
Aの関数からget_2aが急に無くなってしまい、B.pyが急に動かなくなってしまいました。
SさんはAのプログラムについて全く知らないため、何が起こったか全然分かりません。KさんもBのプログラムに触れられないため、不具合が起きているかどうか知る由もありません。
こんな些細な変化でも依存性逆転ができていないコードだとこのようなことに陥ってしまい、エラーへの対処が遅れてしまうのです。
適用後
from B import AI
class A(AI):
def __init__(self) -> None:
self._a = 10;
self._b = 20;
def get_a2(self) -> int:
return 2*self._a;
def get_3b(self) -> int:
return 3*self._b
from abc import ABCMeta,abstractclassmethod
class AI(metaclass = ABCMeta):
@abstractclassmethod
def get_2a(self) -> int:
pass
class B:
def __init__(self, a: AI) -> None:
self.k = 10
self.a: AI = a
def print_k1(self):
print(self.k + self.a.get_2a())
if __name__ == "__main__":
from A import A
b = B(A())
b.printk1()
ここでもまたSさんは問題に対し対処することができません。
しかしAはAIを継承しているため、KさんがAを用いて何らかのテストをした際に、実装が正しくできていないとエラーを吐いてくれるのです。
これによりKさんが素早く問題に対処できるようになるのです。
B側が新しい関数 get_sum を必要とした場合
次にSさんがAの_aと_bの合計を得る関数 get_sum を必要とした場合を考えます。
適用前
class A:
def __init__(self) -> None:
self._a = 10;
self._b = 20;
def get_2a(self) -> int:
return 2*self._a;
def get_3b(self) -> int:
return 3*self._b
from A import A
class B:
def __init__(self) -> None:
self.k = 10
self.a = A()
def print_k1(self):
print(self.k + self.a.get_2a())
if __name__ == "__main__":
b = B()
b.printk1()
SさんはAのプログラムに対して何もできないため、Kさんに頼んで実装してもらう他ありません。
ここでSさんは実装されるまでの間、get_sum を使った機能を作ってテストすることはできません。そのためKさんの実装が遅れるとSさんの開発にも影響が出てきてしまいます。
適用後
from B import AI
class A(AI):
def __init__(self) -> None:
self._a = 10;
self._b = 20;
def get_2a(self) -> int:
return 2*self._a;
def get_3b(self) -> int:
return 3*self._b
from abc import ABCMeta,abstractclassmethod
class AI(metaclass = ABCMeta):
@abstractclassmethod
def get_2a(self) -> int:
pass
@abstractclassmethod
def get_sum(self) -> int:
pass
class B:
def __init__(self, a: AI) -> None:
self.k = 10
self.a: AI = a
def print_k1(self):
print(self.k + self.a.get_2a())
if __name__ == "__main__":
from A import A
b = B(A())
b.printk1()
AIの中に get_sum を作ります。実装自体はまたKさんに頼む他ありません。
しかしSさんはすでにAIがあるため、AIを継承した適当なクラスを外部から注入することで、Aの実装を待たずに get_sum を使った機能を作り、テストすることができます。
これにより開発をよりスムーズに進めさせることができるのです。
これって個人開発で役に立つの?
今までの説明でチーム開発における利点は説明できたと思われます。
しかし、個人開発においてはどうなのか。
実は個人開発においてはこの原則はあまり効果を持ちません。何故なら全てのファイルを一人の開発者が作成、編集してしまっているからです。
この原則は複数人で一つのプロジェクトに取り組む際、他の人がファイルをいじった際の影響力を低くし、依存しているクラスの実装に関係なく開発に取り組めるようにするための原則です。
そのため、全てのファイルを一人で作る個人開発ではあまり役に立たないのです。
ただ適用していると他のクラスにエラーがあっても、実装したクラスに影響させずに開発を進められるというメリットはあると思います。