4-5
変更は新しいインスタンスで返す
状態を変えたいときに自分自身を書き換えるのではなく、変更後の値を持つ新しいインスタンスを生成して返す。元のインスタンスはそのまま残るので、変更前後の値を同時に扱えるうえ、生成時のバリデーションが毎回必ず通る。
現金出納帳の残高更新
✕ Bad
class CashBalance:
def __init__(self, amount: int) -> None:
self.amount = amount
def withdraw(self, value: int) -> None:
self.amount -= value # 上書きした時点で変更前の残高は消える
opening = CashBalance(50_000)
opening.withdraw(30_000)
opening.withdraw(40_000)
print(opening.amount) # -20_000 — どの出金で超過したか痕跡がない✓ Good
from dataclasses import dataclass
@dataclass(frozen=True)
class CashBalance:
amount: int
def __post_init__(self) -> None:
if self.amount < 0:
raise ValueError("残高がマイナスになる取引はできません")
def withdraw(self, value: int) -> "CashBalance":
return CashBalance(self.amount - value)
def deposit(self, value: int) -> "CashBalance":
return CashBalance(self.amount + value)
opening = CashBalance(50_000)
closing = opening.withdraw(30_000)
print(opening.amount, closing.amount) # 50_000 と 20_000 が両方残る
closing.withdraw(40_000) # 生成時チェックで即 ValueError@dataclass(frozen=True) は属性への代入を FrozenInstanceError にするので、不変が言語レベルで守られる。新インスタンス生成のたびに __post_init__ のバリデーションが走るため、不正な残高は存在自体が許されない。月初残高と月末残高を別インスタンスとして並べられるのは、帳簿のように履歴を突き合わせるドメインと特に相性がよい。