密結合
8-6

値オブジェクトと DTO を混ぜない

値オブジェクトは1つの概念の値と検証だけを持つ。「関係ありそうだから」と別概念の計算メソッドを足していくと、高凝集のつもりが密結合になる。逆に、層をまたいでデータを運ぶだけの DTO に業務計算を持たせるのも同じ混線。それぞれの役割を守り、別概念の値が必要なら元の値を引数で渡して導出する。

01 請求金額クラスに増殖する別概念の計算

Bad

源泉徴収・振込手数料・ポイントという別概念の計算が請求金額クラスに同居

計算メソッドが増殖した値オブジェクト
@dataclass(frozen=True)
class InvoiceAmount:
    amount: int

    def withholding_tax(self) -> int:
        return int(self.amount * 0.1021)  # 源泉徴収は別概念

    def transfer_fee(self) -> int:
        return 0 if self.amount >= 30_000 else 330  # 振込手数料も別概念

    def reward_point(self) -> int:
        return self.amount // 100  # ポイントも別概念
Good

概念ごとに値オブジェクトを立て、請求金額は引数として渡す

概念1つにつき値オブジェクト1つ
@dataclass(frozen=True)
class InvoiceAmount:
    amount: int

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


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

    @classmethod
    def on(cls, invoice: InvoiceAmount) -> 'WithholdingTax':
        return cls(int(invoice.amount * 0.1021))


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

    @classmethod
    def on(cls, invoice: InvoiceAmount) -> 'TransferFee':
        return cls(0 if invoice.amount >= 30_000 else 330)

「請求金額に関係する計算だから」は同居の理由にならない。源泉徴収率の改定で請求金額クラスを触るのは責務の混線。元の値をコンストラクタ引数で受け取れば、概念どうしは疎結合のまま連携できる。

02 画面表示用 DTO への業務ロジック混入

Bad

データを運ぶだけのはずの DTO が割引の再計算を始めている

業務計算を持ってしまった DTO
@dataclass
class InvoiceSummaryDto:
    client_name: str
    amount: int
    issued_on: date

    def apply_discount(self) -> None:
        # 運搬役のはずが業務ルールを抱え、しかも自分を書き換える
        self.amount = int(self.amount * 0.9)
Good

DTO は不変の運搬専用に保ち、計算は値オブジェクト側で済ませる

運搬に徹する DTO
@dataclass(frozen=True)
class InvoiceSummaryDto:
    client_name: str
    amount: int  # 割引計算は InvoiceAmount 側で済ませてから詰める
    issued_on: date

DTO に計算を持たせると、同じ業務ルールが表示層とドメイン層に二重に存在し、片方だけ直す事故が起きる。「このクラスは運ぶだけ」と割り切れているかを設計の境界線にする。

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

値オブジェクトと DTO を混ぜない

値オブジェクトは1つの概念の値と検証だけを持つ。「関係ありそうだから」と別概念の計算メソッドを足していくと、高凝集のつもりが密結合になる。逆に、層をまたいでデータを運ぶだけの DTO に業務計算を持たせるのも同じ混線。それぞれの役割を守り、別概念の値が必要なら元の値を引数で渡して導出する。

01 請求金額クラスに増殖する別概念の計算

Bad

源泉徴収・振込手数料・ポイントという別概念の計算が請求金額クラスに同居

計算メソッドが増殖した値オブジェクト
@dataclass(frozen=True)
class InvoiceAmount:
    amount: int

    def withholding_tax(self) -> int:
        return int(self.amount * 0.1021)  # 源泉徴収は別概念

    def transfer_fee(self) -> int:
        return 0 if self.amount >= 30_000 else 330  # 振込手数料も別概念

    def reward_point(self) -> int:
        return self.amount // 100  # ポイントも別概念
Good

概念ごとに値オブジェクトを立て、請求金額は引数として渡す

概念1つにつき値オブジェクト1つ
@dataclass(frozen=True)
class InvoiceAmount:
    amount: int

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


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

    @classmethod
    def on(cls, invoice: InvoiceAmount) -> 'WithholdingTax':
        return cls(int(invoice.amount * 0.1021))


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

    @classmethod
    def on(cls, invoice: InvoiceAmount) -> 'TransferFee':
        return cls(0 if invoice.amount >= 30_000 else 330)

「請求金額に関係する計算だから」は同居の理由にならない。源泉徴収率の改定で請求金額クラスを触るのは責務の混線。元の値をコンストラクタ引数で受け取れば、概念どうしは疎結合のまま連携できる。

02 画面表示用 DTO への業務ロジック混入

Bad

データを運ぶだけのはずの DTO が割引の再計算を始めている

業務計算を持ってしまった DTO
@dataclass
class InvoiceSummaryDto:
    client_name: str
    amount: int
    issued_on: date

    def apply_discount(self) -> None:
        # 運搬役のはずが業務ルールを抱え、しかも自分を書き換える
        self.amount = int(self.amount * 0.9)
Good

DTO は不変の運搬専用に保ち、計算は値オブジェクト側で済ませる

運搬に徹する DTO
@dataclass(frozen=True)
class InvoiceSummaryDto:
    client_name: str
    amount: int  # 割引計算は InvoiceAmount 側で済ませてから詰める
    issued_on: date

DTO に計算を持たせると、同じ業務ルールが表示層とドメイン層に二重に存在し、片方だけ直す事故が起きる。「このクラスは運ぶだけ」と割り切れているかを設計の境界線にする。

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