密結合
8-1

責務ごとにクラスを分ける(単一責任)

あるクラスの計算ロジックが「動いているから」と別の用途に流用されると、両者は運命共同体になる。片方の仕様変更がもう片方を壊し、修正のたびに影響範囲の調査が必要になる。1つのクラスが背負う変更理由は1つに絞り、概念ごとに独立したクラスを立てるのが単一責任の考え方。

顧問報酬の割引計算

Bad

長期契約割引が初年度割引の計算メソッドを「流用」していて、初年度割引の変更に巻き込まれる

他クラスのロジックに相乗りした割引計算
class FirstYearFeeService:
    def discounted(self, base_amount: int) -> int:
        if base_amount < 0:
            raise ValueError("報酬額が不正です")
        # 初年度は 5,000 円引き
        return max(base_amount - 5_000, 0)


class LongTermFeeService:
    def discounted(self, base_amount: int) -> int:
        # 「検証も値引きも済んでいるから」と初年度割引の結果を拝借
        first_year = FirstYearFeeService().discounted(base_amount)
        return int(first_year * 0.9)  # 長期契約はさらに10%引き
Good

割引の種類ごとに値オブジェクトを立てる。どちらの仕様変更も相手に波及しない

概念ごとに独立した値オブジェクト
@dataclass(frozen=True)
class AdvisoryFee:
    amount: int

    def __post_init__(self) -> None:
        if self.amount < 0:
            raise ValueError("報酬額は0以上にしてください")


@dataclass(frozen=True)
class FirstYearFee:
    amount: int

    @classmethod
    def of(cls, base: AdvisoryFee) -> 'FirstYearFee':
        return cls(max(base.amount - 5_000, 0))


@dataclass(frozen=True)
class LongTermFee:
    amount: int

    @classmethod
    def of(cls, base: AdvisoryFee) -> 'LongTermFee':
        return cls(int(base.amount * 0.9))

Bad 版で「初年度割引を 8,000 円引きに変更」すると、長期契約の請求額まで黙って変わる。クラスが増えるのを恐れて相乗りさせるより、概念1つにつきクラス1つの方が変更はずっと安全。

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

責務ごとにクラスを分ける(単一責任)

あるクラスの計算ロジックが「動いているから」と別の用途に流用されると、両者は運命共同体になる。片方の仕様変更がもう片方を壊し、修正のたびに影響範囲の調査が必要になる。1つのクラスが背負う変更理由は1つに絞り、概念ごとに独立したクラスを立てるのが単一責任の考え方。

顧問報酬の割引計算

Bad

長期契約割引が初年度割引の計算メソッドを「流用」していて、初年度割引の変更に巻き込まれる

他クラスのロジックに相乗りした割引計算
class FirstYearFeeService:
    def discounted(self, base_amount: int) -> int:
        if base_amount < 0:
            raise ValueError("報酬額が不正です")
        # 初年度は 5,000 円引き
        return max(base_amount - 5_000, 0)


class LongTermFeeService:
    def discounted(self, base_amount: int) -> int:
        # 「検証も値引きも済んでいるから」と初年度割引の結果を拝借
        first_year = FirstYearFeeService().discounted(base_amount)
        return int(first_year * 0.9)  # 長期契約はさらに10%引き
Good

割引の種類ごとに値オブジェクトを立てる。どちらの仕様変更も相手に波及しない

概念ごとに独立した値オブジェクト
@dataclass(frozen=True)
class AdvisoryFee:
    amount: int

    def __post_init__(self) -> None:
        if self.amount < 0:
            raise ValueError("報酬額は0以上にしてください")


@dataclass(frozen=True)
class FirstYearFee:
    amount: int

    @classmethod
    def of(cls, base: AdvisoryFee) -> 'FirstYearFee':
        return cls(max(base.amount - 5_000, 0))


@dataclass(frozen=True)
class LongTermFee:
    amount: int

    @classmethod
    def of(cls, base: AdvisoryFee) -> 'LongTermFee':
        return cls(int(base.amount * 0.9))

Bad 版で「初年度割引を 8,000 円引きに変更」すると、長期契約の請求額まで黙って変わる。クラスが増えるのを恐れて相乗りさせるより、概念1つにつきクラス1つの方が変更はずっと安全。

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