関係し合うデータとロジックをクラスに
金額データを生の数値や dict で持ち回り、それを操作するロジックをあちこちの関数に分散させると、「残高は0未満にならない」のような業務ルールの実装漏れや重複が必ず起きる。データと、そのデータを守るルール・操作するロジックは1つのクラスにまとめる。これが凝集度を高める設計の基本形だ。
01 経費プリペイドカードの残高管理
# 残高データはただの dict で持ち回る
card = {"balance": 30_000}
def spend(card: dict, amount: int) -> None:
card["balance"] -= amount
# マイナス残高を防ぐガードを書き忘れている
def charge(card: dict, amount: int) -> None:
card["balance"] += amount
if card["balance"] > 100_000: # 上限ルールはこちらにだけある
card["balance"] = 100_000from dataclasses import dataclass
from typing import ClassVar
@dataclass(frozen=True)
class CardBalance:
MIN: ClassVar[int] = 0
MAX: ClassVar[int] = 100_000
value: int
def __post_init__(self) -> None:
if not (self.MIN <= self.value <= self.MAX):
raise ValueError(f"残高は{self.MIN}〜{self.MAX}円: {self.value}")
def spend(self, amount: int) -> "CardBalance":
return CardBalance(max(self.value - amount, self.MIN))
def charge(self, amount: int) -> "CardBalance":
return CardBalance(min(self.value + amount, self.MAX))Java の final フィールドに相当する不変性は、Python では @dataclass(frozen=True) で表現できる。ルールがクラス内に集まっているので、ガードの書き忘れや重複実装が構造的に起きない。
02 使う側のコードはどうなるか
balance = CardBalance(30_000)
after_taxi = balance.spend(4_500) # CardBalance(value=25500)
after_charge = after_taxi.charge(80_000) # 上限で 100,000 に補正
# 元の balance は 30,000 のまま残っている
CardBalance(-100) # ValueError: 不正な残高は生成すらできない「変更は新しいインスタンスで返す」設計の利点は第4章(不変の活用)で詳しく扱う。ここではまず、データとロジックが同じ場所にあると使う側が安全になる、という効果を押さえたい。