設計の初歩
2-4

関係し合うデータとロジックをクラスに

金額データを生の数値や dict で持ち回り、それを操作するロジックをあちこちの関数に分散させると、「残高は0未満にならない」のような業務ルールの実装漏れや重複が必ず起きる。データと、そのデータを守るルール・操作するロジックは1つのクラスにまとめる。これが凝集度を高める設計の基本形だ。

01 経費プリペイドカードの残高管理

Bad

残高はただの dict、操作ロジックは別々の関数。下限0のガードが片方に抜けている

データとルールが離れている
# 残高データはただの 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_000
Good

残高の値・上下限ルール・操作を1つの不変クラスに集約。不正な残高はそもそも作れない

データとロジックを凝集させたクラス
from 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 使う側のコードはどうなるか

Good

操作のたびに新しいインスタンスが返るので、変更前の値が壊れず履歴も追いやすい

不変クラスの利用例
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章(不変の活用)で詳しく扱う。ここではまず、データとロジックが同じ場所にあると使う側が安全になる、という効果を押さえたい。

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

関係し合うデータとロジックをクラスに

金額データを生の数値や dict で持ち回り、それを操作するロジックをあちこちの関数に分散させると、「残高は0未満にならない」のような業務ルールの実装漏れや重複が必ず起きる。データと、そのデータを守るルール・操作するロジックは1つのクラスにまとめる。これが凝集度を高める設計の基本形だ。

01 経費プリペイドカードの残高管理

Bad

残高はただの dict、操作ロジックは別々の関数。下限0のガードが片方に抜けている

データとルールが離れている
# 残高データはただの 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_000
Good

残高の値・上下限ルール・操作を1つの不変クラスに集約。不正な残高はそもそも作れない

データとロジックを凝集させたクラス
from 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 使う側のコードはどうなるか

Good

操作のたびに新しいインスタンスが返るので、変更前の値が壊れず履歴も追いやすい

不変クラスの利用例
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章(不変の活用)で詳しく扱う。ここではまず、データとロジックが同じ場所にあると使う側が安全になる、という効果を押さえたい。

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