Skip to main content

ドメイン集約で薄いラッパー被覆はアンチパターン

TL;DR​

  • 既に別ドメインが責務を持っている API を、別の companion(集約ドメイン)に 薄くラップし直すだけの再ラッパーは価値がない。単に呼び先を一段増やすだけで、抽象も不変条件も追加しない。
  • 「集約」には 価値のある集約(散らばった実装・判断を 1 か所に寄せる)と alias 的ラッパー(既存 API を名前だけ替えて素通しする)が混在しやすい。この 2 つを仕分ける。
  • 判断基準:そのラッパーが 新しい責務・不変条件・重複の解消を持つなら残す。持たないなら既存ドメインを直接使うほうが良い。

遭遇した問題​

src/domains/task/ を新設し、既存の @/domains/broken-link / @/domains/parse-error の API を、Task companion に薄くラップして集約した。

// 新設した集約ドメイン(Task companion)
export const Task = {
// 既存ドメインの API をそのまま呼び直しているだけ
hasBrokenLinks: (task: Task) => BrokenLink.exists(task.links),
hasParseIssues: (task: Task) => ParseError.exists(task.content),
// ...
};

Task.hasBrokenLinks / Task.hasParseIssues は、broken-link / parse-error ドメインが既に持っている判定を 名前を替えて呼び直しているだけだった。

原因​

  • 既存ドメイン(broken-link / parse-error)が 既にその責務を持っている。それを別 companion(Task)に再ラップしても、抽象の階層が増えるだけで新しい価値は生まれない。
  • 一方で、同じ「集約」の中には 価値のある変更も混ざっていた。例:displayTitle の算出ロジックが 12 箇所の callsite に散っていたのを Task companion に集約する、というものは重複解消として意味がある。
  • つまり「ドメインを集約する」という作業の中に、value のある集約と alias 的ラッパーが同居しており、これを仕分けないまま一律に集約してしまっていた。

解決​

  • 価値のある集約は維持する(例:displayTitle の 12 callsite 集約)。散っていた実装・判断を 1 か所に寄せるものは、重複を減らし変更点を局所化するので残す。
  • alias 的なラッパーは撤去し、既存ドメイン(broken-link / parse-error)を 直接利用する形に戻した。
// Before: Task 経由で素通し
if (Task.hasBrokenLinks(task)) { ... }

// After: 責務を持つドメインを直接使う
if (BrokenLink.exists(task.links)) { ... }

教訓・判断基準​

集約 companion に API を足すかどうかは、次で仕分ける。

  • 残す(価値のある集約):散らばったロジックを 1 か所に寄せる/重複を消す/新しい不変条件・判断を持たせる。
  • 足さない(alias 的ラッパー):既に別ドメインが責務を持つ API を、名前を替えて呼び直すだけ。抽象も不変条件も追加しない素通し。

既に別ドメインが責務を持っている API を、別の companion に再ラップしても価値がない。集約の名の下に「素通しラッパー」を増やすと、階層だけが深くなり、どこに本当の責務があるのか分かりにくくなる。