可変オブジェクトを返さない
戻り値の設計はメソッドの出口の設計。内部で持っている可変のリストや辞書をそのまま返すと、呼び出し側の何気ない操作が内部状態を壊す。また素の int や float で返すと、戻ってきた値が何の金額なのか分からなくなり取り違えが起きる。戻り値は不変のスナップショットや、意図を語る不変の独自型で返す。
01 仕訳帳の内部リストが外から壊される
class JournalBook:
def __init__(self) -> None:
self._entries: list[JournalEntry] = []
def add(self, entry: JournalEntry) -> None:
self._entries.append(entry)
def entries(self) -> list[JournalEntry]:
return self._entries # 内部リストへの参照が漏れる
book = JournalBook()
view = book.entries()
view.clear() # 画面表示用に空にしたつもりが、帳簿本体が消えるclass JournalBook:
def __init__(self) -> None:
self._entries: list[JournalEntry] = []
def add(self, entry: JournalEntry) -> None:
self._entries.append(entry)
@property
def entries(self) -> tuple[JournalEntry, ...]:
return tuple(self._entries) # 変更しようとすればエラーになるlist をコピーして返す手もあるが、tuple なら「変更できない」ことを型で宣言できる。辞書なら types.MappingProxyType、独自クラスなら frozen な dataclass が同じ役割を果たす。
02 金額を素の int で返すと意味が消える
def total_sales(items: list[SalesItem]) -> int:
return sum(i.amount for i in items)
def consumption_tax(amount: int) -> int:
return amount * 10 // 110
sales = total_sales(items) # 税込売上
tax = consumption_tax(sales) # 消費税額
net = sales - tax # 税抜売上
register_monthly_sales(tax) # 本当は net を渡すべき。int 同士なので通る@dataclass(frozen=True)
class ConsumptionTax:
amount: int
@dataclass(frozen=True)
class NetSales:
amount: int
@dataclass(frozen=True)
class GrossSales:
amount: int
def tax(self) -> ConsumptionTax:
return ConsumptionTax(self.amount * 10 // 110)
def net(self) -> NetSales:
return NetSales(self.amount - self.tax().amount)
def register_monthly_sales(net: NetSales) -> None: ...
register_monthly_sales(GrossSales(1_100_000).net()) # 型が意図を保証frozen=True なら戻り値が後から書き換えられる心配もない。「不変の独自型を返す」だけで、可変漏れと値の取り違えの両方を一度に塞げる。