ソフトウェア構築時に従うべき5つのガイドライン。ソフトウェアの拡張性や保守性を高めるためのもの。
オブジェクト指向プログラミングを使っても、「分かりやすいプログラム」や「メンテナンスしやすいプログラム」になるというわけではない。そこで、5つのガイドラインによって、開発者にとって読みやすくメンテナンスが可能なプログラムを作成するための指標が生み出された。
S:SRP [ Single Responsibility Principle] 【単一責任の原則】
プログラム内の各クラスの役割を1つに絞るということ。したがって同一クラスに異なる理由で変更を加えるメソッドを入れてはならない。すべてのクラスはソフトウェアが提供する機能の1部分だけの役割を果たすべきであり、コード変更時の影響範囲を最小限に抑える。
-
やるとどうなるのか?
- システムの凝集性が高まる
- オブジェクトが役割がはっきりして、整理される
-
やらないとどうなるのか?
-
変更を加えると他のメソッドにも影響が伝播する可能性がある
- リファクタが難しくなる
- ゴッドクラス(肥大化したクラス)になり、全体の把握が難しくなる
-
変更を加えると他のメソッドにも影響が伝播する可能性がある
(ただし、やりすぎるとバラバラになりどこに何が書いてあるかわからなくなってしまうため、適度にやっていきましょう)
×
- 本クラスに、本の内容を伝えてくれるメソッドと、内容を保存してくれるメソッドがある
- ファイルではなく、データベースを保存する場合は、Bookクラスを書き換える必要が出てくる。
- 本の内容をとる、保存するという二つの役割があるので、分離したほうが良い。
class Book:
def __init__(self, name: str, author_name: str):
self.__name = name
self.__author_name = author_name
def get_name(self) -> str:
return self.__name
def get_author_name(self) -> str:
return self.__author_name
def save(self) -> None:
with open("book.txt") as f:
f.write(self.get_name() + " " + self.get_author_name())
if __name__ == "__main__":
book = Book("A Great Book", "Naoki Uehara")
book.save()
○
- Bookクラスは本の内容を持つ
- BookRepositoryクラスは本の内容を保存してくれる。
- Bookの役割を分離でき、スマートになった。
class Book:
def __init__(self, name: str, author_name: str):
self.__name = name
self.__author_name = author_name
def get_name(self) -> str:
return self.__name
def get_author_name(self) -> str:
return self.__author_name
class BookRepository:
def save(self, book: Book) -> None:
with open("book.txt", "w") as f:
f.write(book.get_name() + "," + book.get_author_name())
if __name__ == "__main__":
book_repository = BookRepository()
book = Book("A Great Book", "Naoki Uehara")
book_repository.save(book)
O:OCP [Open-Closed Principle]【オープン・クローズドの原則】
クラスを修正することなく、クラスの振る舞いを拡張できなければならないというもの。すなわち、なにか機能を追加するとき、新しいクラスを追加することは自由であるが、既存クラスの変更は最小限に留めなければならない。
次のコードの中に、CalculateAreasというクラスがある。ここでは、Rectangleオブジェクトを受け取り、オブジェクトの計算をしてその結果を返却している。
×
もし、円の面積の計算を加えたいのであれば、CalculateAreasクラスを変更しなければならない。これは、既存クラスの変更を最小限に留めなければならないというオープン/クローズの原則に反している。
class Rectangle:
def __init__(self, length: float, width: float):
self.__length = length
self.__width = width
def get_length(self):
return self.__length
def get_width(self):
return self.__width
class CalculateAreas:
def calcArea(self, r: Rectangle):
return r.get_width() * r.get_length()
if __name__ == "__main__":
r = Rectangle(1, 2)
ca = CalculateAreas()
print(ca.calcArea(r))
○
上記の実装では、新しい図形のクラスを加えてもCalculateAreasクラスを変更する必要が無い。
このようにすることで、安全にコードを拡張することができる。オープン/クローズの原則を一言で言えば、「サブクラスを通して拡張することで元のクラスを変更する必要がなくなる」ということである。
from abc import ABC
# インターフェース
class Shape(ABC):
@abstractmethod
def get_area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, length: float, width: float):
self.__length = length
self.__width = width
def get_area(self) -> float:
return self.__length * self.__width
class Circle(Shape):
def __init__(self, r: float):
self.__radius = r
def get_area(self) -> float:
return self.__radius * self.__radius * 3.14
class CalculateAreas:
def calcArea(self, shape: Shape):
return shape.get_area()
if __name__ == "__main__":
ca = CalculateAreas()
r = Rectangle(1, 2)
print(ca.calcArea(r))
c = Circle(4)
print(ca.calcArea(c))
L:LSP [Liskov Substitution Principle]【リスコフの置換原則】
親クラスのインスタンスは、その子クラスのインスタンスによって置き換えることができるように設計しなければならないというもの。親クラスが何かを行うことができるのであれば、子クラスも同じことができなくてはならない。つまり一貫性を持たせるということ。
×
- Rectangle(長方形クラス)とSquare(正方形クラス)がある。正方形クラスは縦の長さ=横の長さという制約がある。リスコフ原則によれば、Rectangleクラスを正方形クラスに置換しても正しく動作することが必要だが、先の制約からうまく動作しない。
from abc import ABC
# 図形インターフェース
class Shape(ABC):
def calc_area(self) -> float:
pass
# 長方形クラス
class Rectangle(Shape):
def __init__(self, length: float, width: float):
self.__length = length
self.__width = width
def calc_area(self) -> float:
return self.__length * self.__width
def set_length(self, length: float):
self.__length = length
def set_width(self, width: float):
self.__width = width
# 正方形クラス
class Square(Rectangle):
def __init__(self, length):
super().set_length(length)
super().set_width(length)
def set_length(self, length: float):
super().set_length(length)
super().set_width(length)
def set_width(self, length: float):
super().set_length(length)
super().set_width(length)
if __name__ == "__main__":
r = Rectangle(1, 2)
print(r.calc_area())
s = Square(4)
print(s.calc_area())
○
- 例えば、継承が合わないのなら委譲を使う。
from abc import ABC
# 図形インターフェース
class Shape(ABC):
def calc_area(self) -> float:
pass
# 長方形クラス
class Rectangle(Shape):
def __init__(self, length: float, width: float):
self.__length = length
self.__width = width
def calc_area(self) -> float:
return self.__length * self.__width
def set_length(self, length: float):
self.__length = length
def set_width(self, width: float):
self.__width = width
# 正方形クラス
class Square(Shape):
def __init__(self, length):
self.__rectangle = Rectangle(length, length)
def set_length(self, length: float):
self.__rectangle.set_length(length)
self.__rectangle.set_width(length)
def calc_area(self):
self.__rectangle.calc_area()
if __name__ == "__main__":
r = Rectangle(1, 2)
print(r.calc_area())
s = Square(4)
print(s.calc_area())
I:ISP [Interface Segregation Principle]【インターフェイス分離の原則】
一つのインターフェースに全てまとめるのではなく、小さなインターフェースに分けた方がいいというものである。
-
やらないとどうなるのか?
- 呼び出し元はそのインターフェースしか見えないので、呼び出しちゃいけないメソッドを呼び出してしまう。
- インターフェースが大きすぎると具象クラスは単一責任の原則に反してしまう。
×
動物インターフェースを定義する。動物インターフェースは走る、食べる、ミルクを出すメソッドを必要とする。
from abc import ABC,abstractmethod
# 走る&食べる&ミルクを出すことができれば動物インターフェースを実装しているとみなす
class animal(ABC):
@abstractmethod
def run(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def milk(self):
pass
犬クラスは動物インターフェースを実装できる。
# 犬は走る&食べる&ミルクを出すことができる
class dog(animal):
def run(self):
print("run")
def eat(self):
print("eat")
def milk(self):
print("milk")
トカゲクラスを実装しようとすると、走る、食べることはできるが、爬虫類のためミルクを出すことはできない。このように大きなインターフェースを作ると、具象クラスは使用しないメソッドを実装することを強いられる。
# トカゲはミルクを出すことができないので、milkは実装できない。
class lizard(animal):
def run(self):
print("run")
def eat(self):
print("eat")
def milk(self):
# このメソッドを呼ぶことは禁止する!!!!
pass
全体のソースコード
from abc import ABC,abstractmethod
# 走る&食べる&ミルクを出すことができれば動物インターフェースを実装しているとみなす
class animal(ABC):
@abstractmethod
def run(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def milk(self):
pass
# 犬は走る&食べる&ミルクを出すことができる
class dog(animal):
def run(self):
print("run")
def eat(self):
print("eat")
def milk(self):
print("cry")
# トカゲはミルクを出すことができないので、milkは実装できない。
class lizard(animal):
def run(self):
print("run")
def eat(self):
print("eat")
def milk(self):
# このメソッドを呼ぶことは禁止する!!!!
pass
def func(a: animal):
a.milk()
func(dog())
func(lizard())
○
animalインターフェースが大きすぎたので分割する。ミルクを出すメソッドをmammalインターフェースに分けることにした。
from abc import ABC,abstractmethod
# 走る&食べることができれば動物インターフェースを実装しているとみなす
class animal(ABC):
@abstractmethod
def run(self):
pass
@abstractmethod
def eat(self):
pass
# ミルクを出すことができれば哺乳類インターフェースを実装しているとみなす
class mammal(ABC):
@abstractmethod
def milk(self):
pass
犬クラスは、動物インターフェースと哺乳類インターフェースを実装することにした。
# 犬は動物かつ哺乳類である
class dog(animal, mammal):
def run(self):
print("run")
def eat(self):
print("eat")
def milk(self):
print("cry")
トカゲクラスは動物インターフェースを実装すれば良い。
# トカゲは動物である
class lizard(animal):
def run(self):
print("run")
def eat(self):
print("eat")
全体のソースコード
from abc import ABC,abstractmethod
# 走る&食べることができれば動物インターフェースを実装しているとみなす
class animal(ABC):
@abstractmethod
def run(self):
pass
@abstractmethod
def eat(self):
pass
# ミルクを出すことができれば哺乳類インターフェースを実装しているとみなす
class mammal(ABC):
@abstractmethod
def milk(self):
pass
# 犬は動物かつ哺乳類である
class dog(animal, mammal):
def run(self):
print("run")
def eat(self):
print("eat")
def milk(self):
print("cry")
# トカゲは動物である
class lizard(animal):
def run(self):
print("run")
def eat(self):
print("eat")
def func(a: animal):
a.eat()
func(dog())
func(lizard())
D:DIP [Dependency Inversion Principle]【依存関係逆転の原則】
普通にコードを書いてしまうと、処理の方向と依存の方向が同じになってしまい、抽象が具象に依存してしまう。インターフェースを用いて依存の向きを逆転してやる。
-
やるとどうなるのか?
-
変更頻度の低いインターフェースに依存させることで、呼び出し元へ変更の影響が伝播しにくくなる
- 基本的には変更頻度が低いオブジェクトに依存させたほうが、書き換えの範囲が少なくて済む
-
具象クラスを変更すれば振る舞いを変更できる(ポリモーフィズム)
- ファイルに保存する・DBに保存する・メモリの保存するクラスなど別々にクラスを作っておき、使いたいクラスをインターフェースに代入すれば振る舞いを切り替えられる。
-
振る舞いを変更できると言うことはテストが書ける
- わざとエラーを発生させることで、エラーが発生したときのテストが書ける
-
モックが刺せる
- 一時的に固定値を返すクラスを作って、インターフェースに代入することで、DB等を用意しなくとも開発を進められる
-
変更頻度の低いインターフェースに依存させることで、呼び出し元へ変更の影響が伝播しにくくなる
-
やらないとどうなるのか?
- 技術的な理由で振る舞いを変更するため具象クラスを変更したときに抽象も変更しないといけなくなる。
×
二つのクラスの依存関係は以下のような形になっている。ここで問題なのは、UserRegisterUsecaseという上位オブジェクトがUserRepositoryに依存しており、抽象が具象に依存してしまっている。
UserRegisterUsecase
UserRegisterUsecaseはユーザー登録の責務を負うクラス。UserRepositoryを使ってユーザー情報を保存する。
class UserRegisterUsecase:
def __init__(self):
self.__user_repository = FileUserRepository()
def handle(self, user_id : int, user_name:str) -> None:
self.__user_repository.create(str(user_id), user_name)
FileUserRepository
FileUserRepositoryはcreateを公開しており、ユーザーをファイルに保存できる。
class FileUserRepository:
def __init__(self):
self.__file_name = "user.txt"
def create(self, user_id : int, user_name:str) -> None:
with open(self.__file_name, "a", encoding='utf-8', newline='\n') as f:
f.write(str(user_id) + " " + user_name + "\n")
利用者側はhandleメソッドを使ってユーザー登録できる。
if __name__ == "__main__":
user_register_usecase = UserRegisterUsecase()
user_register_usecase.handle(0, "uehara")
user_register_usecase.handle(1, "nakahara")
user_register_usecase.handle(2, "simohara")
全体のコード
class UserRegisterUsecase:
def __init__(self):
self.__user_repository = FileUserRepository()
def handle(self, user_id : int, user_name:str) -> None:
self.__user_repository.create(str(user_id), user_name)
class FileUserRepository:
def __init__(self):
self.__file_name = "user.txt"
def create(self, user_id : int, user_name:str) -> None:
with open(self.__file_name, "a", encoding='utf-8', newline='\n') as f:
f.write(str(user_id) + " " + user_name + "\n")
def find_by_id(self, user_id:int) -> str:
with open(self.__file_name, "r") as f:
for i in f.readlines():
if len(i) >= 2 and i[0] == user_id:
return i[1]
return ""
if __name__ == "__main__":
user_register_usecase = UserRegisterUsecase()
user_register_usecase.handle(0, "uehara")
user_register_usecase.handle(1, "nakahara")
user_register_usecase.handle(2, "simohara")
○
具象から抽象に依存の向きを変える必要がある。インターフェースを使えば依存の向きを自由に変えることができる。ユーザー情報を保存するインターフェースを作ると、FileUserRepository(具象)がUserRepositoryInterface(抽象)に依存する形になった。
UserRepositoryInterface
createができればUserRepositoryInterfaceとして扱える。
class UserRepositoryInterface(ABC):
def create(self, user_id :int, user_name : str) -> None:
pass
FileUserRepository
UserRepositoryInterfaceを実装する(継承)。振る舞いとしてはユーザーIDとユーザー名を受け取り、ファイルに保存する。
class FileUserRepository(UserRepositoryInterface):
def __init__(self):
self.__file_name = "user.txt"
def create(self, user_id : int, user_name : str) -> None:
with open(self.__file_name, "a", encoding='utf-8', newline='\n') as f:
f.write(str(user_id) + " " + user_name + "\n")
UserRegisterUsecase
コンストラクタでUserRepositoryInterfaceを実装したクラスを受け取るようにする。こうすることで、どのように保存するか、その形式をどうするか?などの具体的な振る舞いを気にしなくても良くなる。
class UserRegisterUsecase:
def __init__(self, user_repository : UserRepositoryInterface):
self.__user_repository = user_repository
def handle(self, user_id : int, user_name : str) -> None:
self.__user_repository.create(user_id, user_name)
main
どのUserRepositoryを使うか決めて、ユースケースに渡す。外からインターフェース(抽象)に具象クラスをあてがうので、 依存性の注入(DI) と呼ばれている。
if __name__ == "__main__":
# DI
user_repository = FileUserRepository()
user_register_usecase = UserRegisterUsecase(user_repository)
# ユースケースの実行
user_register_usecase.handle(0, "uehara")
user_register_usecase.handle(1, "nakahara")
user_register_usecase.handle(2, "simohara")
まとめ