第41章 アンチパターン③:Repositoryが何でも屋になる 🧹😵
![hex_ts_study_041[(./picture/hex_ts_study_041_anti_pattern_god_repository.png)
(Past chat)(Past chat)(Past chat)(Past chat)(Past chat)
第41章:アンチパターン③「Repositoryが何でも屋になる」🧹😵💫💥
1) この章のゴール 🎯✨
この章が終わったら、こんなことができるようになるよ〜!😊🌸
- 「あ、これ 何でも屋Repository だ…!」って気づける👀⚡
- Repository(=Outbound Port🔌)を ちょうど良いサイズ に保てる✂️✨
- 肥大化したRepositoryを 手順どおりに分解 して直せる🔧🧩
- 結果として、中心(UseCase/Domain)が守られてテストも楽になる🧪💕
※いまのTypeScriptは npm の “latest” が 5.9.3 になってるよ(2026-01時点の表示)。(NPM) Node.js は v24(LTS: Krypton) がActive LTSで、2026-01にも更新が出てる感じ。(Node.js)
2) 何でも屋Repositoryって、どんな状態? 🐘🍔💦
ありがちなストーリー📖💭
最初はこうだったのに…
TodoRepositoryにsave()とfindById()くらいしか無い✨
だんだん要望が増えて、こうなる👇😇
completeTodoAndNotifySlack()addTodoIfTitleUniqueOtherwiseThrow()findTodosWithUserAndTagsAndLatestComments()bulkUpdateStatusesAndLogAndRetry()
……もう、DB担当なのか業務担当なのか通知担当なのか わかんないやつ😵💫💥
3) 何が困るの?(地味に致命傷)🧨😱
困りごと①:中心(UseCase/Domain)が汚れる 🥲
Repositoryが強くなりすぎると、UseCaseが薄くなって 「中心はルール置き場🏠」が崩れちゃうの💦
困りごと②:テストが急にしんどい 🧪😭
Repositoryが “何でも” やってると、テストで差し替えたい範囲が広すぎて モックが地獄になる…(そしてテストを書かなくなる…)😇
困りごと③:変更が怖い(影響範囲がでかい)😵
「一覧画面の表示をちょい変えるだけ」なのに Repository 触ることになって 別のUseCaseが壊れる みたいな事故が起きがち⚠️💥
4) 症状チェックリスト✅(3つ当てはまったら黄色信号🚦)
-
Repositoryのメソッド数が 10個超えてる(しかも増え続ける)📈😇
-
メソッド名に 業務動詞 が入ってる
complete,notify,validate,reopen,assign,approveなど👀
-
返り値が 画面用DTO とか JOIN結果の盛り合わせ になってる🍱
-
Repositoryの中に
if (title === "") throw ...みたいな ルール判定 がある🚫 -
「とりあえずRepositoryに置いとくか」って空気がある(最重要)🫠
5) ダメな例:巨大Repository(何でも屋)🐘🍔
「うわ…あるある…」ってなるやつ置いとくね😇💦
// ❌ 何でも屋Repository(アンチパターン)
export interface TodoRepository {
// 永続化っぽい
save(todo: any): Promise<void>;
findById(id: string): Promise<any>;
// いつの間にか業務ルール
addTodoIfTitleUnique(title: string): Promise<void>;
completeTodoAndPreventDoubleApply(id: string): Promise<void>;
// いつの間にか外部I/O
completeTodoAndNotifySlack(id: string): Promise<void>;
// いつの間にか画面都合(JOIN盛り盛り)
findTodosWithUserAndTagsAndLatestComments(): Promise<any[]>;
}
これ、何がヤバいかというと… Port(約束🔌)が “中心の言葉” じゃなくなってる のが一番キツいの😵💫
6) 正しい感覚:Repositoryは「永続化の都合」を隠す係 💾🧩
Repository(=Outbound Port🔌)の役割は、ざっくりこれ👇
- ✅ 保存する
- ✅ 取り出す
- ✅ 永続化の都合(ファイル/DB/HTTPなど)を隠す
- ❌ ルールを決める(例:タイトル空禁止)
- ❌ 状態遷移を判断する(例:二重完了禁止)
- ❌ 通知する(Slack/メール等)
- ❌ 画面都合の集計やJOIN盛り合わせを背負う
7) 直し方のコツ:UseCaseの言葉に寄せて「分割」✂️✨
コツ①:まず「ユースケースの台本」を思い出す🎬✨
ToDoなら中心はだいたいこれだったよね📝
- AddTodo
- CompleteTodo
- ListTodos
Repositoryが何でも屋になったら、いったん 「台本に必要な道具だけ」 に戻すのが最短ルート🚀
コツ②:Portは “使う側(UseCase)” が欲しい最小だけ🔌✂️
たとえば、読みと書きを分けるだけでも一気にスッキリするよ😊
// ✅ 読み取り専用(Query寄り)
export interface TodoReaderPort {
findById(id: TodoId): Promise<Todo | null>;
findAll(): Promise<Todo[]>;
}
// ✅ 書き込み専用(Command寄り)
export interface TodoWriterPort {
save(todo: Todo): Promise<void>;
}
UseCaseは「必要な方だけ」受け取れるから、依存が細くなる✨🧠
コツ③:画面向けの “盛り合わせ” は Query 用のPortに逃がす🍱➡️🧩
「一覧で、ユーザー名もタグもコメントも全部ほしい!」みたいなのは Repositoryに押し込まない のが大事💦
代わりにこうする👇
// ✅ 画面用の取得は、別Portにする(Read Model / Query Service)
export interface TodoListQueryPort {
listForDisplay(): Promise<Array<{
id: string;
title: string;
completed: boolean;
// 表示に必要な分だけ
// userName?: string; tags?: string[]; ...
}>>;
}
これでRepositoryは「ドメインの保管庫」っぽさを保てるし、 画面は画面で最適化できるよ🌸✨
8) 「ルールはどこへ行くの?」→ 中心だよ🛡️❤️
たとえば「二重完了禁止」はRepositoryに入れたくなるけど… それはドメイン or ユースケースが担当 だよ😊
export class CompleteTodoUseCase {
constructor(
private readonly reader: TodoReaderPort,
private readonly writer: TodoWriterPort,
) {}
async execute(id: TodoId): Promise<void> {
const todo = await this.reader.findById(id);
if (!todo) throw new Error("Todo not found"); // ※実際は章33/34のエラー設計に合わせてね🧯
// ✅ ルールは中心で判断(例:二重完了禁止)
const completed = todo.complete(); // domainの状態遷移
await this.writer.save(completed);
}
}
Repositoryは「保存したり取り出したり」だけ。 判断(ルール)は中心でやる。これが気持ちいい分担💕🧠
9) “Port増えすぎ問題” を防ぐ小ワザ🧯✨
「分割は大事」だけど、増やしすぎても迷子になるよね😵💫 判断基準を置いとくね👇
- ✅ 同じ集まり(同じAggregate) を扱ってるなら、まとめてもOK
- ✅ 同じUseCaseが毎回セットで使うなら、まとめてもOK
- ❌ 画面都合のJOINや集計が混ざるなら、分ける
- ❌ 通知・時間・UUIDが混ざるなら、分ける(Clock/IdGenerator/Notifierなど)
10) Adapter側はどうなる?(実装は “薄い” まま)🥗✨
Adapterは、Portを実装するだけ🧩 読み書き2つのPortを同じAdapterが実装しても全然OKだよ〜😊
export class FileTodoRepositoryAdapter implements TodoReaderPort, TodoWriterPort {
async findById(id: TodoId): Promise<Todo | null> {
// JSON読み取り → domainへ変換(変換だけ!)
return null;
}
async findAll(): Promise<Todo[]> {
return [];
}
async save(todo: Todo): Promise<void> {
// domain → JSONへ変換して保存(変換だけ!)
}
}
ここに if (title==="") とか 二重完了チェック を入れたくなったら、
「それ中心の仕事だよ〜!🛡️」って自分に言って止めよ😂✨
11) AI拡張での “事故防止” プロンプト集🤖📝✅
コピペで使えるやつ置いとくね💕
- 「この
TodoRepositoryは何でも屋になってない?理由も添えて、分割案を3つ出して」 - 「Repository内に業務ルールが混ざってたら指摘して。どこへ移すべきか(UseCase/Domain/Adapter)も提案して」
- 「このPortは大きすぎる? ‘ユースケースが本当に必要な最小’ に削った場合のインターフェース案を作って」
- 「Query(画面用)とCommand(更新用)が混ざってたら、分離案(Reader/Writer or QueryPort)を出して」
12) ミニ実習:今あるRepositoryを分解してみよ🧩🔧✨
お題📝
あなたの TodoRepository(または想定)に、次のメソッドが混ざってるとする👇
completeTodoAndNotifySlackfindTodosWithUserAndTags...addTodoIfTitleUnique
やること(3ステップ)🚀
- 「保存/取得」だけ残す(Repository本体)💾
- 通知は
NotifierPortにする📨 - 画面用一覧は
TodoListQueryPortにする🍱
終わったら、UseCase側の依存がスッキリしてるのを眺めてニヤニヤしよ😊💕
まとめ:Repositoryが何でも屋になったらこう思って🧠💡
- Repositoryは 永続化の都合を隠す係 💾🧩
- ルールは 中心(UseCase/Domain) 🛡️❤️
- 画面都合の盛り合わせは Query用Port 🍱✨
- Portは ユースケースが欲しい最小 に寄せる🔌✂️
次の章では、こういう “分解” をもっと手早くやるための 「直し方テンプレ」🔧📌 をガッツリ使っていくよ〜!😊🎀