単体で正常動作するクラスをつくる
クラスは「データ」と「そのデータを守り、正しく操作するメソッド」の両方を備えてはじめて一人前になる。初期化やバリデーションを呼び出し側に任せるデータ置き場のようなクラスは、未初期化の中途半端なインスタンスや不正値の混入を許し、関連ロジックがあちこちに散らばる原因になる。家電製品が箱から出してすぐ安全に使えるように、クラスも生成した瞬間から正しく使える状態で渡すのが基本。
01 請求書クラスの初期化を呼び出し側に任せない
@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 の請求書が発行される@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 計算ロジックをデータの持ち主に寄せる
# 画面表示モジュール
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))@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()データとロジックが同じクラスに集まっている状態を高凝集と呼ぶ。逆に「データはここ、計算はあちこち」と散らばった低凝集な構造は、重複コード・修正漏れ・探し回るコストの温床になる。