名前設計
10-4

状態の違いを名前で区別する

下書き・発行済み・入金済みなど、状態によって意味も許される操作も変わるのに、1つのクラス名・メソッド名で済ませると、名前から想像できない動作が紛れ込む。呼び出し側が驚かないよう、状態の違いと状態の変更は名前に昇格させる。

01 合計を尋ねただけなのに状態が変わる

Bad

total_amount という名前からは読み取れない「督促手数料の加算」という状態変更が隠れている

名前と中身が食い違う
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 と状態を名前にすれば、「下書きに入金記録する」ようなバグはそもそも書けなくなる。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第10章。コード例は原則を自分の題材で表現し直したオリジナル。
10-4

状態の違いを名前で区別する

下書き・発行済み・入金済みなど、状態によって意味も許される操作も変わるのに、1つのクラス名・メソッド名で済ませると、名前から想像できない動作が紛れ込む。呼び出し側が驚かないよう、状態の違いと状態の変更は名前に昇格させる。

01 合計を尋ねただけなのに状態が変わる

Bad

total_amount という名前からは読み取れない「督促手数料の加算」という状態変更が隠れている

名前と中身が食い違う
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 と状態を名前にすれば、「下書きに入金記録する」ようなバグはそもそも書けなくなる。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第10章。コード例は原則を自分の題材で表現し直したオリジナル。