私がオブジェクト指向を勉強するにあたって役立った講座三部作を書く。
これらの講座を作ったのはピーコック アンダーソンである。
- オブジェクト指向の原則1:単一責務の原則とオープンクローズドの原則
- オブジェクト指向の原則2:リスコフの置換原則と継承以外の解決方法
- オブジェクト指向の原則3:依存関係逆転の原則とインタフェース分離の原則
これらを学ぶと、オブジェクト指向において陥りがちなミスに対して、適切に対応することが可能である。
よくありがちなミスが、Aという具象クラスを継承して拡張し、Bというクラスを作るというもの。これのどこがまずいかというと、BはAとしても振る舞える必要がある(リスコフの置換原則)が、それが壊れるからである。
こういう問題が起きないパターンとしては、Aがインタフェースであるか、抽象クラスであり、かつAの具象メソッドをオーバーライドしていない場合である。この場合、Bに期待されるのは、Aの抽象メソッドがこう振る舞うべきであるという規定通りに動くことである。
from abc import ABC, abstractmethod
class A(ABC):
"""抽象クラス"""
@abstractmethod
def f(self, i: int) -> int:
"""整数に何らかの加工を行って返します."""
pass
class B(A):
"""具象クラス"""
def f(self, i: int) -> int:
"""整数に2倍して返します."""
return i * 2
c: A = B()
print(c.f(3))
例えば、これはこの原則に従えば正しいことは明らかだろう。ところが、例えばAが具象クラスであり、fがiを3倍して返す関数だったとしたらどうだろうか?容易に想像がつくが、3倍して返す関数であるという挙動を想定してプログラムを組んでしまい、容易に破綻するのである。
ほかにも、抽象に対してプログラミングすべき、という規則もある。全部具象クラスで組むのであれば、継承という概念は無意味であるのだから当然である。
例として、DbStorageというクラスはIStorageというインタフェースを実装しており、データベースに接続して値を取得して返す仕組みであるとする。この場合、DbStorageは具体的にデータベースに接続して値を取得するわけだが、DbStorageに依存して組んでしまうと、データベースに接続しないとテストもできないし、データベースの中身に応じて動きが変わってしまうなどの問題が発生する。このため、MockStorageという、別のIStorageを実装したクラスを用意して、IStorageの想定通りに振る舞うようにすると、テストがやりやすくなるため、非常に便利になるのだ。
もしかしたら少し上級者向けかもしれないが、受けてみるのは一つの手段だろう。