オブジェクト指向設計における5つの原則の頭文字。すべて「変更に強いクラス・モジュール構造」を目指すという同じ目的を持ち、 結合度と凝集度 を具体的な設計指針まで落とし込んだものと捉えられる。
| 略称 | 原則 | 一言でいうと |
|---|---|---|
| SRP | 単一責任の原則 | クラスを変更する理由は1つにする |
| OCP | オープン・クローズドの原則 | 拡張には開き、修正には閉じる |
| LSP | リスコフの置換原則 | サブクラスは親クラスと置き換え可能であるべき |
| ISP | インターフェース分離の原則 | 使わないメソッドへの依存を強制しない |
| DIP | 依存性逆転の原則 | 抽象に依存し、具象に依存しない |
SRP: Single Responsibility Principle¶
単一責任の原則。「クラスを変更する理由は1つだけであるべき」というもの。ここでいう「責任」はメソッドの数ではなく、 変更の理由(誰の要求で変わるか) を指す。
例
Report クラスが「内容の計算」と「PDF出力フォーマット」の両方を持っていると、業務ロジックの変更と出力形式の変更という
別々の理由でこのクラスが書き換わることになる。
# 責任が混在した例:集計ロジックと出力形式が同じクラスに同居
class Report:
def calculate_total(self, items):
return sum(item.price for item in items)
def to_pdf(self, items):
total = self.calculate_total(items)
return f"PDF: total={total}" # 出力フォーマットの詳細
# 責任を分離した例
class ReportCalculator:
def calculate_total(self, items):
return sum(item.price for item in items)
class PdfReportFormatter:
def format(self, total):
return f"PDF: total={total}"SRPを狭く適用しすぎるとクラスが過剰に分裂し、逆に追いづらくなる。「同じ理由で変わる部分をまとめる」というのが本質で、 機械的にメソッドを1個ずつクラスに分けることではない。
OCP: Open-Closed Principle¶
オープン・クローズドの原則。
コードは次の2つの性質を満たすように設計すべきというもの
拡張に対して開いている(Open for extension)→ 新しい振る舞いを追加できるようにしておく。
修正に対して閉じている(Closed for modification)→ 既存のコードを変更せずに、新しい機能を実現できるようにする。
※どのコードでもOCPを適用すると、変更可能性は高まるが複雑で冗長になる。変更されなかったら無駄に冗長なだけになってしまう。変更を予測しすぎず、変更が多いことがわかったら対応するくらいでいい
例
ログの出力方式を指定してログを出せるようにしたいとする。
同じ関数に入れて条件分岐する場合、新しい出力方法を追加するたびに関数を修正する必要があり、閉じていない
# OCPに従わない実装例
def log(message, log_type="console"):
if log_type == "console":
print(message)
elif log_type == "file":
with open("log.txt", "a") as f:
f.write(message + "\n")
elif log_type == "slack":
send_to_slack(message)
# 新しい出力方法を追加するたびに、この関数を修正する必要あり↓はStrategyパターン(同じインターフェースをもつ関数を作って使用時に選択するパターン)を使ってOCPに従うよう実装した例
# OCP準拠例
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message): pass
class ConsoleLogger(Logger):
def log(self, message):
print(message)
class FileLogger(Logger):
def log(self, message):
with open("log.txt", "a") as f:
f.write(message + "\n")
class SlackLogger(Logger):
def log(self, message):
send_to_slack(message)
# 使用例
def run_process(logger: Logger):
logger.log("処理を開始します")LSP: Liskov Substitution Principle¶
リスコフの置換原則。「サブタイプは、そのスーパータイプと置き換えても、プログラムの正しさを壊してはならない」というもの。
例
Rectangle を継承した Square が、親クラスの契約(幅と高さを独立に設定できる)を壊す典型例。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, w):
self.width = w
def set_height(self, h):
self.height = h
def area(self):
return self.width * self.height
# LSP違反:Squareは幅と高さを独立に変更できないので、親の契約を壊す
class Square(Rectangle):
def set_width(self, w):
self.width = self.height = w
def set_height(self, h):
self.width = self.height = h
def resize_and_check(rect: Rectangle):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # Squareを渡すと 16 になり失敗する「見た目は自然な継承関係(正方形は長方形の一種)」でも、振る舞いの契約が壊れるなら継承すべきではない、という判断基準を与える。 違反を見つけたら継承をやめて委譲やコンポジションに置き換えることが多い。
ISP: Interface Segregation Principle¶
インターフェース分離の原則。「クライアントは、自分が使わないメソッドへの依存を強制されるべきではない」というもの。
例
from abc import ABC, abstractmethod
# ISP違反:どんなワーカーにもscan()を実装させてしまう
class Worker(ABC):
@abstractmethod
def work(self): ...
@abstractmethod
def eat(self): ...
@abstractmethod
def scan(self): ... # ロボットワーカーには不要
# ISP準拠:役割ごとにインターフェースを分ける
class Workable(ABC):
@abstractmethod
def work(self): ...
class Eatable(ABC):
@abstractmethod
def eat(self): ...
class Scannable(ABC):
@abstractmethod
def scan(self): ...
class HumanWorker(Workable, Eatable):
def work(self): print("working")
def eat(self): print("eating")
class RobotWorker(Workable, Scannable):
def work(self): print("working")
def scan(self): print("scanning")肥大化した「何でもインターフェース」を作ると、実装クラスが不要なメソッドのダミー実装を持つ羽目になる。 用途ごとに小さなインターフェースへ分割することで、無関係な変更の影響を受けにくくなる。
DIP: Dependency Inversion Principle¶
依存性逆転の原則。「上位モジュールは下位モジュールに依存すべきではなく、両者とも抽象に依存すべき」というもの。 「抽象は詳細に依存してはならず、詳細が抽象に依存すべき」とも表現される。
通常の依存の向き:上位(業務ロジック)→下位(DBアクセスなどの実装詳細)
DIPを適用した依存の向き:上位・下位の両方が、間に挟んだ抽象(インターフェース)に依存する
例
from abc import ABC, abstractmethod
# 抽象(上位・下位の両方がここに依存する)
class NotificationSender(ABC):
@abstractmethod
def send(self, message: str): ...
# 下位モジュール:抽象を実装する(=抽象に依存する側に回る)
class EmailSender(NotificationSender):
def send(self, message: str):
print(f"Email: {message}")
# 上位モジュール:具体的な実装ではなく抽象に依存する
class OrderService:
def __init__(self, sender: NotificationSender):
self.sender = sender
def complete_order(self):
self.sender.send("注文が完了しました")
service = OrderService(EmailSender())
service.complete_order()DIPは 依存性注入(DI) という実装テクニックによって実現されることが多い。 DIPが「あるべき依存の向き」という設計原則で、DIはそれを実現する手段、という関係になる。 また、クリーンアーキテクチャ の「依存関係のルール」もDIPの応用。