8-1
責務ごとにクラスを分ける(単一責任)
あるクラスの計算ロジックが「動いているから」と別の用途に流用されると、両者は運命共同体になる。片方の仕様変更がもう片方を壊し、修正のたびに影響範囲の調査が必要になる。1つのクラスが背負う変更理由は1つに絞り、概念ごとに独立したクラスを立てるのが単一責任の考え方。
顧問報酬の割引計算
✕ Bad
class FirstYearFeeService:
def discounted(self, base_amount: int) -> int:
if base_amount < 0:
raise ValueError("報酬額が不正です")
# 初年度は 5,000 円引き
return max(base_amount - 5_000, 0)
class LongTermFeeService:
def discounted(self, base_amount: int) -> int:
# 「検証も値引きも済んでいるから」と初年度割引の結果を拝借
first_year = FirstYearFeeService().discounted(base_amount)
return int(first_year * 0.9) # 長期契約はさらに10%引き✓ Good
@dataclass(frozen=True)
class AdvisoryFee:
amount: int
def __post_init__(self) -> None:
if self.amount < 0:
raise ValueError("報酬額は0以上にしてください")
@dataclass(frozen=True)
class FirstYearFee:
amount: int
@classmethod
def of(cls, base: AdvisoryFee) -> 'FirstYearFee':
return cls(max(base.amount - 5_000, 0))
@dataclass(frozen=True)
class LongTermFee:
amount: int
@classmethod
def of(cls, base: AdvisoryFee) -> 'LongTermFee':
return cls(int(base.amount * 0.9))Bad 版で「初年度割引を 8,000 円引きに変更」すると、長期契約の請求額まで黙って変わる。クラスが増えるのを恐れて相乗りさせるより、概念1つにつきクラス1つの方が変更はずっと安全。