ファーストクラスコレクション
★★★生の list をあちこちのクラスへ引数で渡して回すと、件数上限や重複禁止といったルールの検査が複数箇所に重複し、検査を通らずに append できる抜け道も生まれる。コレクションを専用クラスで包み、操作ロジックを同じ場所に集約したものがファーストクラスコレクション。不変に設計すれば堅牢さも手に入る。
01 月次仕訳の明細管理
MAX_LINES = 200
class JournalDraftService:
def add_line(self, lines: list[JournalLine], new: JournalLine) -> None:
if len(lines) >= MAX_LINES:
raise ValueError("仕訳明細数が上限です")
lines.append(new)
def assert_balanced(self, lines: list[JournalLine]) -> None:
debit = sum(line.debit_amount for line in lines)
credit = sum(line.credit_amount for line in lines)
if debit != credit:
raise ValueError("貸借が一致していません")
class LedgerPostService:
def total_debit(self, lines: list[JournalLine]) -> int:
# 別のサービスにも同じ list が渡され、直接 append できてしまう
return sum(line.debit_amount for line in lines)@dataclass(frozen=True)
class JournalLines:
MAX_LINES: ClassVar[int] = 200
lines: tuple[JournalLine, ...] = ()
def add(self, new: JournalLine) -> "JournalLines":
if self.is_full:
raise ValueError("仕訳明細数が上限です")
return JournalLines(self.lines + (new,))
@property
def is_full(self) -> bool:
return len(self.lines) == JournalLines.MAX_LINES
@property
def total_debit(self) -> int:
return sum(line.debit_amount for line in self.lines)
@property
def total_credit(self) -> int:
return sum(line.credit_amount for line in self.lines)
@property
def is_balanced(self) -> bool:
return self.total_debit == self.total_creditadd() が新しい JournalLines を返す設計は第4章「変更は新しいインスタンスで返す」の応用。ルールを破った仕訳明細リストはそもそも作れなくなり、検査ロジックの重複も消える。
02 内部コレクションを外に漏らさない
class JournalLines:
def __init__(self) -> None:
self._lines: list[JournalLine] = []
@property
def lines(self) -> list[JournalLine]:
return self._lines # 内部リストへの参照がそのまま漏れる
# 呼び出し側: 上限も貸借一致も検査されない追加経路ができてしまう
journal_lines.lines.append(unchecked_line)class JournalLines:
def __init__(self) -> None:
self._lines: list[JournalLine] = []
@property
def lines(self) -> tuple[JournalLine, ...]:
return tuple(self._lines) # 変更できないコピーを渡すJava の unmodifiableList に相当する発想。Python なら tuple(dict なら types.MappingProxyType)で変更不能なビューを返す。最初から内部を tuple で持つ前セットの設計なら、この心配自体がなくなる。