ファーストクラスコレクション
生の list をあちこちのクラスへ引数で渡して回すと、件数上限や重複禁止といったルールの検査が複数箇所に重複し、検査を通らずに append できる抜け道も生まれる。コレクションを専用クラスで包み、操作ロジックを同じ場所に集約したものがファーストクラスコレクション。不変に設計すれば堅牢さも手に入る。
01 投資ポートフォリオの銘柄管理
MAX_HOLDINGS = 10
class TradeService:
def add_holding(self, holdings: list[Holding], new: Holding) -> None:
if any(h.ticker == new.ticker for h in holdings):
raise ValueError("既に保有している銘柄です")
if len(holdings) >= MAX_HOLDINGS:
raise ValueError("保有銘柄数が上限です")
holdings.append(new)
class ReportService:
def total_value(self, holdings: list[Holding]) -> int:
# 別のサービスにも同じ list が渡され、直接 append できてしまう
return sum(h.value for h in holdings)@dataclass(frozen=True)
class Portfolio:
MAX_HOLDINGS: ClassVar[int] = 10
holdings: tuple[Holding, ...] = ()
def add(self, new: Holding) -> "Portfolio":
if self.contains(new.ticker):
raise ValueError("既に保有している銘柄です")
if self.is_full:
raise ValueError("保有銘柄数が上限です")
return Portfolio(self.holdings + (new,))
def contains(self, ticker: str) -> bool:
return any(h.ticker == ticker for h in self.holdings)
@property
def is_full(self) -> bool:
return len(self.holdings) == Portfolio.MAX_HOLDINGS
@property
def total_value(self) -> int:
return sum(h.value for h in self.holdings)add() が新しい Portfolio を返す設計は第4章「変更は新しいインスタンスで返す」の応用。ルールを破った銘柄リストはそもそも作れなくなり、検査ロジックの重複も消える。
02 内部コレクションを外に漏らさない
class Portfolio:
def __init__(self) -> None:
self._holdings: list[Holding] = []
@property
def holdings(self) -> list[Holding]:
return self._holdings # 内部リストへの参照がそのまま漏れる
# 呼び出し側: 上限も重複も検査されない追加経路ができてしまう
portfolio.holdings.append(unchecked_holding)class Portfolio:
def __init__(self) -> None:
self._holdings: list[Holding] = []
@property
def holdings(self) -> tuple[Holding, ...]:
return tuple(self._holdings) # 変更できないコピーを渡すJava の unmodifiableList に相当する発想。Python なら tuple(dict なら types.MappingProxyType)で変更不能なビューを返す。最初から内部を tuple で持つ前セットの設計なら、この心配自体がなくなる。