ドメイン集約で薄いラッパー被覆はアンチパターン
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 に散っていたのをTaskcompanion に集約する、というものは重複解消として意味がある。 - つまり「ドメインを集約する」という作業の中に、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 に再ラップしても価値がない。集約の名の下に「素通しラッパー」を増やすと、階層だけが深くなり、どこに本当の責務があるのか分かりにくくなる。