クラス設計
3-1

単体で正常動作するクラスをつくる

クラスは「データ」と「そのデータを守り、正しく操作するメソッド」の両方を備えてはじめて一人前になる。初期化やバリデーションを呼び出し側に任せるデータ置き場のようなクラスは、未初期化の中途半端なインスタンスや不正値の混入を許し、関連ロジックがあちこちに散らばる原因になる。家電製品が箱から出してすぐ安全に使えるように、クラスも生成した瞬間から正しく使える状態で渡すのが基本。

01 請求書クラスの初期化を呼び出し側に任せない

Bad

全フィールドにデフォルト値を持つ入れ物クラス。呼び出し側があとから属性を埋める設計なので、設定し忘れた中途半端なインスタンスがそのまま動いてしまう

呼び出し側が初期化の責任を負う「入れ物」クラス
@dataclass
class Invoice:
    client_name: str = ""
    subtotal: int = 0
    tax_rate: float = 0.0

# 呼び出し側(別モジュール)が属性を1つずつ埋める
invoice = Invoice()
invoice.client_name = client.name
invoice.subtotal = sum(line.amount for line in lines)
# tax_rate の設定を忘れても誰も気づかない
print(invoice.subtotal * invoice.tax_rate)  # 税額 0.0 の請求書が発行される
Good

生成に必要な値をすべてコンストラクタで受け取り、不正値はその場で例外にする。生成できた時点で「正しい請求書」であることが保証される

生成と同時に正常値を保証する
@dataclass(frozen=True)
class Invoice:
    client_name: str
    subtotal: int
    tax_rate: Decimal

    def __post_init__(self) -> None:
        if not self.client_name:
            raise ValueError("請求先名は必須です")
        if self.subtotal < 0:
            raise ValueError("小計には0以上を指定してください")
        if not Decimal("0") <= self.tax_rate <= Decimal("0.10"):
            raise ValueError("税率は0〜10%の範囲で指定してください")

invoice = Invoice(client_name=client.name, subtotal=total, tax_rate=Decimal("0.10"))
# ここまで来たら invoice は必ず正常値を持っている

Python では `__post_init__` がガード節の置き場所になる。不正値を渡すとインスタンス自体が生成できないため、「不正な請求書が存在しない」状態をつくれる。Java のコンパイルエラーのような静的な防御はないが、生成の瞬間に弾く点は同じ考え方。

02 計算ロジックをデータの持ち主に寄せる

Bad

税込金額の計算が請求書クラスの外に書かれている。同じ計算が画面表示・PDF出力・仕訳起票の3箇所に重複し、端数処理の修正漏れが起きる

計算ロジックが呼び出し側に散らばる
# 画面表示モジュール
display_total = invoice.subtotal + int(invoice.subtotal * invoice.tax_rate)

# PDF出力モジュール(こちらは四捨五入で実装されてしまった)
pdf_total = invoice.subtotal + round(invoice.subtotal * invoice.tax_rate)

# 仕訳起票モジュール(さらにもう1つの実装)
journal_amount = int(invoice.subtotal * (1 + invoice.tax_rate))
Good

税込金額の計算を Invoice 自身のメソッドにする。端数処理のルールが1箇所に決まり、利用側は結果を受け取るだけになる

データを持つクラスに計算を持たせる
@dataclass(frozen=True)
class Invoice:
    client_name: str
    subtotal: int
    tax_rate: Decimal

    def total_with_tax(self) -> int:
        """税込金額。端数は切り捨てで統一する"""
        tax = int(self.subtotal * self.tax_rate)
        return self.subtotal + tax

# 画面表示・PDF出力・仕訳起票のどこからでも同じ結果になる
display_total = invoice.total_with_tax()

データとロジックが同じクラスに集まっている状態を高凝集と呼ぶ。逆に「データはここ、計算はあちこち」と散らばった低凝集な構造は、重複コード・修正漏れ・探し回るコストの温床になる。

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

単体で正常動作するクラスをつくる

クラスは「データ」と「そのデータを守り、正しく操作するメソッド」の両方を備えてはじめて一人前になる。初期化やバリデーションを呼び出し側に任せるデータ置き場のようなクラスは、未初期化の中途半端なインスタンスや不正値の混入を許し、関連ロジックがあちこちに散らばる原因になる。家電製品が箱から出してすぐ安全に使えるように、クラスも生成した瞬間から正しく使える状態で渡すのが基本。

01 請求書クラスの初期化を呼び出し側に任せない

Bad

全フィールドにデフォルト値を持つ入れ物クラス。呼び出し側があとから属性を埋める設計なので、設定し忘れた中途半端なインスタンスがそのまま動いてしまう

呼び出し側が初期化の責任を負う「入れ物」クラス
@dataclass
class Invoice:
    client_name: str = ""
    subtotal: int = 0
    tax_rate: float = 0.0

# 呼び出し側(別モジュール)が属性を1つずつ埋める
invoice = Invoice()
invoice.client_name = client.name
invoice.subtotal = sum(line.amount for line in lines)
# tax_rate の設定を忘れても誰も気づかない
print(invoice.subtotal * invoice.tax_rate)  # 税額 0.0 の請求書が発行される
Good

生成に必要な値をすべてコンストラクタで受け取り、不正値はその場で例外にする。生成できた時点で「正しい請求書」であることが保証される

生成と同時に正常値を保証する
@dataclass(frozen=True)
class Invoice:
    client_name: str
    subtotal: int
    tax_rate: Decimal

    def __post_init__(self) -> None:
        if not self.client_name:
            raise ValueError("請求先名は必須です")
        if self.subtotal < 0:
            raise ValueError("小計には0以上を指定してください")
        if not Decimal("0") <= self.tax_rate <= Decimal("0.10"):
            raise ValueError("税率は0〜10%の範囲で指定してください")

invoice = Invoice(client_name=client.name, subtotal=total, tax_rate=Decimal("0.10"))
# ここまで来たら invoice は必ず正常値を持っている

Python では `__post_init__` がガード節の置き場所になる。不正値を渡すとインスタンス自体が生成できないため、「不正な請求書が存在しない」状態をつくれる。Java のコンパイルエラーのような静的な防御はないが、生成の瞬間に弾く点は同じ考え方。

02 計算ロジックをデータの持ち主に寄せる

Bad

税込金額の計算が請求書クラスの外に書かれている。同じ計算が画面表示・PDF出力・仕訳起票の3箇所に重複し、端数処理の修正漏れが起きる

計算ロジックが呼び出し側に散らばる
# 画面表示モジュール
display_total = invoice.subtotal + int(invoice.subtotal * invoice.tax_rate)

# PDF出力モジュール(こちらは四捨五入で実装されてしまった)
pdf_total = invoice.subtotal + round(invoice.subtotal * invoice.tax_rate)

# 仕訳起票モジュール(さらにもう1つの実装)
journal_amount = int(invoice.subtotal * (1 + invoice.tax_rate))
Good

税込金額の計算を Invoice 自身のメソッドにする。端数処理のルールが1箇所に決まり、利用側は結果を受け取るだけになる

データを持つクラスに計算を持たせる
@dataclass(frozen=True)
class Invoice:
    client_name: str
    subtotal: int
    tax_rate: Decimal

    def total_with_tax(self) -> int:
        """税込金額。端数は切り捨てで統一する"""
        tax = int(self.subtotal * self.tax_rate)
        return self.subtotal + tax

# 画面表示・PDF出力・仕訳起票のどこからでも同じ結果になる
display_total = invoice.total_with_tax()

データとロジックが同じクラスに集まっている状態を高凝集と呼ぶ。逆に「データはここ、計算はあちこち」と散らばった低凝集な構造は、重複コード・修正漏れ・探し回るコストの温床になる。

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