メソッド設計
12-2

可変オブジェクトを返さない

戻り値の設計はメソッドの出口の設計。内部で持っている可変のリストや辞書をそのまま返すと、呼び出し側の何気ない操作が内部状態を壊す。また素の int や float で返すと、戻ってきた値が何の金額なのか分からなくなり取り違えが起きる。戻り値は不変のスナップショットや、意図を語る不変の独自型で返す。

01 仕訳帳の内部リストが外から壊される

Bad

内部の仕訳リストをそのまま返したため、呼び出し側の clear() で帳簿本体が消えた

可変の内部状態をそのまま返す
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()  # 画面表示用に空にしたつもりが、帳簿本体が消える
Good

tuple に詰め替えた不変のスナップショットを返す。呼び出し側がどう扱っても帳簿は無傷

不変のスナップショットを返す
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 で返すと意味が消える

Bad

税込売上・消費税・税抜売上が全部 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 同士なので通る
Good

金額の種類ごとに frozen dataclass の独自型をつくる。渡し間違いは実行前に型チェッカーが弾く

不変の独自型で戻り値の意図を表明する
@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 なら戻り値が後から書き換えられる心配もない。「不変の独自型を返す」だけで、可変漏れと値の取り違えの両方を一度に塞げる。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第12章。コード例は原則を自分の題材で表現し直したオリジナル。
12-2

可変オブジェクトを返さない

戻り値の設計はメソッドの出口の設計。内部で持っている可変のリストや辞書をそのまま返すと、呼び出し側の何気ない操作が内部状態を壊す。また素の int や float で返すと、戻ってきた値が何の金額なのか分からなくなり取り違えが起きる。戻り値は不変のスナップショットや、意図を語る不変の独自型で返す。

01 仕訳帳の内部リストが外から壊される

Bad

内部の仕訳リストをそのまま返したため、呼び出し側の clear() で帳簿本体が消えた

可変の内部状態をそのまま返す
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()  # 画面表示用に空にしたつもりが、帳簿本体が消える
Good

tuple に詰め替えた不変のスナップショットを返す。呼び出し側がどう扱っても帳簿は無傷

不変のスナップショットを返す
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 で返すと意味が消える

Bad

税込売上・消費税・税抜売上が全部 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 同士なので通る
Good

金額の種類ごとに frozen dataclass の独自型をつくる。渡し間違いは実行前に型チェッカーが弾く

不変の独自型で戻り値の意図を表明する
@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 なら戻り値が後から書き換えられる心配もない。「不変の独自型を返す」だけで、可変漏れと値の取り違えの両方を一度に塞げる。

参考: 『良いコード/悪いコードで学ぶ設計入門』(ミノ駆動 著、技術評論社)第12章。コード例は原則を自分の題材で表現し直したオリジナル。