10-4
状態の違いを名前で区別する
下書き・発行済み・入金済みなど、状態によって意味も許される操作も変わるのに、1つのクラス名・メソッド名で済ませると、名前から想像できない動作が紛れ込む。呼び出し側が驚かないよう、状態の違いと状態の変更は名前に昇格させる。
01 合計を尋ねただけなのに状態が変わる
✕ Bad
class Invoice:
def total_amount(self) -> int:
total = sum(line.amount for line in self.lines)
# 期日超過なら督促手数料を加算し、フィールドも書き換える
if date.today() > self.due_date:
self.late_fee = 500
total += self.late_fee
return total✓ Good
class Invoice:
def total_amount(self) -> int:
return sum(line.amount for line in self.lines)
def is_overdue(self, today: date) -> bool:
return today > self.due_date
def with_late_fee(self) -> "OverdueInvoice":
return OverdueInvoice(self, late_fee=500)「驚き最小の原則」。名前から想像できる以上のことを中でやり始めたら、その仕事には別の名前を与える。
02 状態そのものをクラス名にする
✓ Good
@dataclass(frozen=True)
class DraftInvoice:
lines: tuple[InvoiceLine, ...]
def issue(self, number: str, due_date: date) -> "IssuedInvoice":
return IssuedInvoice(self.lines, number, due_date)
@dataclass(frozen=True)
class IssuedInvoice:
lines: tuple[InvoiceLine, ...]
number: str
due_date: date
def record_payment(self, paid_on: date) -> "PaidInvoice":
return PaidInvoice(self, paid_on)状態フラグの if 分岐で読み分けさせる代わりに、DraftInvoice / IssuedInvoice / PaidInvoice と状態を名前にすれば、「下書きに入金記録する」ようなバグはそもそも書けなくなる。