データクラスが不具合を呼び寄せる
★★★フィールドを公開してデータを持つだけのクラスは、一見シンプルで無害に見える。しかしデータを使う計算ロジックが外に散らばるため、重複コード・修正漏れ・可読性低下を招き、さらに未初期化のまま使える「生焼けオブジェクト」や不正値の混入まで許してしまう。1つの悪しき構造が複数の不具合を連鎖的に呼び込む典型例。
01 商品有高帳の残高クラスと散らばる金額計算
@dataclass
class InventoryBalance:
product_code: str = ""
quantity: int = 0
unit_cost: Decimal | None = None
class InventoryReportService:
def ending_amount(self, balance: InventoryBalance) -> Decimal:
return balance.unit_cost * balance.quantity
class CostOfSalesService:
# 別の担当者が「未実装だ」と思い込んで再実装した重複コード
def ending_inventory(self, balance: InventoryBalance) -> Decimal:
return balance.unit_cost * balance.quantity@dataclass
class InventoryBalance:
product_code: str
quantity: int
unit_cost: Decimal
def book_value(self) -> Decimal:
"""商品有高帳の帳簿価額(計算ロジックの置き場はここ1か所だけ)"""
return self.unit_cost * self.quantity
class InventoryReportService:
def ending_amount(self, balance: InventoryBalance) -> Decimal:
return balance.book_value()
class CostOfSalesService:
def ending_inventory(self, balance: InventoryBalance) -> Decimal:
# 同じメソッドを再利用するだけ。重複も再実装も起きない
return balance.book_value()データと計算ロジックが離れていると、既存実装の存在に気づけず同じロジックが量産される。データが持つべき計算はデータの近くに置く(凝集)と、仕様変更時の修正点が1か所に収まる。
02 未初期化のまま使えてしまう(生焼けオブジェクト)
balance = InventoryBalance() # 全フィールドが既定値のまま生成できる
# unit_cost は None のまま → ここで TypeError
ending_amount = balance.unit_cost * balance.quantity@dataclass
class InventoryBalance:
product_code: str # 既定値なし → 必須引数
quantity: int # 必須
unit_cost: Decimal # 必須
# InventoryBalance() # TypeError: missing 3 required positional arguments
balance = InventoryBalance(product_code="A-001", quantity=120, unit_cost=Decimal("850"))
# unit_cost は必ず初期化されている → 安全に計算できる
ending_amount = balance.unit_cost * balance.quantity「初期化してから使う」というルールを利用側の記憶に頼る設計は、いつか必ず破られる。生成時に完成形を要求すれば、クラス自身が未初期化状態を許さない構造になる。
03 不正値の混入を構造で防ぐ
balance.quantity = -300 # 商品有高帳の残高数量がマイナス
balance.unit_cost = Decimal("-1200") # 取得単価がマイナス
# 利用側それぞれで防衛的チェックを書き始めると、
# 今度はバリデーションロジック自体が重複コードになる@dataclass(frozen=True)
class InventoryBalance:
product_code: str
quantity: int
unit_cost: Decimal
def __post_init__(self) -> None:
if self.quantity < 0:
raise ValueError("残高数量は0以上であること")
if self.unit_cost < 0:
raise ValueError("取得単価は0以上であること")
def book_value(self) -> Decimal:
return self.unit_cost * self.quantityデータを持つクラス自身が正しさを保証し、計算も自分で担う。この「値オブジェクト+不変」の設計は第3章で詳しく扱う。