データクラスが不具合を呼び寄せる
フィールドを公開してデータを持つだけのクラスは、一見シンプルで無害に見える。しかしデータを使う計算ロジックが外に散らばるため、重複コード・修正漏れ・可読性低下を招き、さらに未初期化のまま使える「生焼けオブジェクト」や不正値の混入まで許してしまう。1つの悪しき構造が複数の不具合を連鎖的に呼び込む典型例。
01 保有銘柄クラスと散らばる評価額ロジック
@dataclass
class Holding:
ticker: str = ""
quantity: int = 0
average_cost: float | None = None
class PortfolioReportService:
def evaluate(self, h: Holding, price: float) -> float:
return price * h.quantity
class RebalanceService:
# 別の担当者が「未実装だ」と思い込んで再実装した重複コード
def current_value(self, h: Holding, price: float) -> float:
return price * h.quantityデータと計算ロジックが離れていると、既存実装の存在に気づけず同じロジックが量産される。評価方法の仕様変更時、全部を見つけて直せなければバグになる。
02 未初期化のまま使えてしまう(生焼けオブジェクト)
holding = Holding() # 全フィールドが既定値のまま生成できる
# average_cost は None のまま → ここで TypeError
profit = (1_850.0 - holding.average_cost) * holding.quantity「初期化してから使う」というルールを利用側の記憶に頼る設計は、いつか必ず破られる。クラス自身が未初期化状態を許さない構造にすべき。
03 不正値の混入を構造で防ぐ
holding.quantity = -300 # 空売りしていないのに負の数量
holding.average_cost = -1200.0 # 取得単価がマイナス
# 利用側それぞれで防衛的チェックを書き始めると、
# 今度はバリデーションロジック自体が重複コードになる@dataclass(frozen=True)
class Holding:
ticker: str
quantity: int
average_cost: Decimal
def __post_init__(self) -> None:
if self.quantity <= 0:
raise ValueError("数量は1以上であること")
if self.average_cost <= 0:
raise ValueError("取得単価は正の値であること")
def market_value(self, price: Decimal) -> Decimal:
return price * self.quantityデータを持つクラス自身が正しさを保証し、計算も自分で担う。この「値オブジェクト+不変」の設計は第3章で詳しく扱う。