不変の活用
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

frozen な値オブジェクトにして、出金後の残高は新しいインスタンスで返す

不変オブジェクト+新インスタンス返却
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__ のバリデーションが走るため、不正な残高は存在自体が許されない。月初残高と月末残高を別インスタンスとして並べられるのは、帳簿のように履歴を突き合わせるドメインと特に相性がよい。

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

frozen な値オブジェクトにして、出金後の残高は新しいインスタンスで返す

不変オブジェクト+新インスタンス返却
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__ のバリデーションが走るため、不正な残高は存在自体が許されない。月初残高と月末残高を別インスタンスとして並べられるのは、帳簿のように履歴を突き合わせるドメインと特に相性がよい。

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