低凝集
5-3

Common/Util クラスの肥大化

「共通で使いそうだから」という理由だけで Common や Util と名付けたクラスに置くと、互いに無関係なロジックが際限なく集まってくる。重要なのは再利用性ではなく関心事。それぞれのロジックを、扱うデータと同じクラスへ引き取らせる。

01 経理ユーティリティの解体

Bad

税込計算・支払期限・マイナンバーのマスクという無関係なロジックが同居している

何でも置き場と化した Util クラス
class AccountingUtil:
    @staticmethod
    def calc_amount_including_tax(amount: Decimal, rate: Decimal) -> Decimal:
        return amount * (1 + rate)

    @staticmethod
    def days_until_due(issued: date, payment_terms: int) -> int:
        return (issued + timedelta(days=payment_terms) - date.today()).days

    @staticmethod
    def mask_my_number(value: str) -> str:
        return "*" * 8 + value[-4:]

    # 「共通っぽいから」で今後も増え続ける…
Good

税込金額という関心事を1つのクラスに。残りも各自のデータ側クラスへ引き取らせる

関心事ごとの値オブジェクトに分解
from dataclasses import dataclass
from decimal import Decimal


@dataclass(frozen=True)
class TaxRate:
    value: Decimal  # 例: Decimal("0.10")


@dataclass(frozen=True)
class AmountIncludingTax:
    value: Decimal

    @classmethod
    def from_excluding_tax(
        cls, amount: Decimal, rate: TaxRate
    ) -> "AmountIncludingTax":
        return cls((amount * (1 + rate.value)).quantize(Decimal("1")))

02 例外: 横断的関心事は共通化してよい

Good

ログ出力のようにあらゆる処理から使われる関心事は、共通モジュールが正解

横断的関心事の共通モジュール
# ログ出力・例外通知・計測などの「横断的関心事」は
# 特定のデータに属さないため、共通モジュールに置いてよい
def report_error(message: str) -> None:
    logger.error(message)
    notify_slack(message)


try:
    ledger.post(entry)
except ValueError:
    report_error("仕訳の記帳に失敗しました")

すべての共通化が悪いわけではない。ログ・エラー通知・キャッシュのように業務データと無関係にどこからでも使われるもの(横断的関心事)だけが、共通モジュールの正当な住人。

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

Common/Util クラスの肥大化

「共通で使いそうだから」という理由だけで Common や Util と名付けたクラスに置くと、互いに無関係なロジックが際限なく集まってくる。重要なのは再利用性ではなく関心事。それぞれのロジックを、扱うデータと同じクラスへ引き取らせる。

01 経理ユーティリティの解体

Bad

税込計算・支払期限・マイナンバーのマスクという無関係なロジックが同居している

何でも置き場と化した Util クラス
class AccountingUtil:
    @staticmethod
    def calc_amount_including_tax(amount: Decimal, rate: Decimal) -> Decimal:
        return amount * (1 + rate)

    @staticmethod
    def days_until_due(issued: date, payment_terms: int) -> int:
        return (issued + timedelta(days=payment_terms) - date.today()).days

    @staticmethod
    def mask_my_number(value: str) -> str:
        return "*" * 8 + value[-4:]

    # 「共通っぽいから」で今後も増え続ける…
Good

税込金額という関心事を1つのクラスに。残りも各自のデータ側クラスへ引き取らせる

関心事ごとの値オブジェクトに分解
from dataclasses import dataclass
from decimal import Decimal


@dataclass(frozen=True)
class TaxRate:
    value: Decimal  # 例: Decimal("0.10")


@dataclass(frozen=True)
class AmountIncludingTax:
    value: Decimal

    @classmethod
    def from_excluding_tax(
        cls, amount: Decimal, rate: TaxRate
    ) -> "AmountIncludingTax":
        return cls((amount * (1 + rate.value)).quantize(Decimal("1")))

02 例外: 横断的関心事は共通化してよい

Good

ログ出力のようにあらゆる処理から使われる関心事は、共通モジュールが正解

横断的関心事の共通モジュール
# ログ出力・例外通知・計測などの「横断的関心事」は
# 特定のデータに属さないため、共通モジュールに置いてよい
def report_error(message: str) -> None:
    logger.error(message)
    notify_slack(message)


try:
    ledger.post(entry)
except ValueError:
    report_error("仕訳の記帳に失敗しました")

すべての共通化が悪いわけではない。ログ・エラー通知・キャッシュのように業務データと無関係にどこからでも使われるもの(横断的関心事)だけが、共通モジュールの正当な住人。

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