第48章:C# Generics を活用した共通リポジトリの罠😇「やりすぎ注意」⚠️

リポジトリを作っていると、だいたい一度はこう思います👇
「
Repository<T>で全部まとめたら、超ラクじゃない?🤩」 「AIにもGenericRepository作らせたら、一瞬で終わるじゃん✨」
うん、気持ちはめっちゃ分かる〜!😂 でもDDDでは、この“ラク”があとで地味に効いてくる罠になりやすいんです🕳️💥
1. なにが「罠」なの?🪤
DDDのリポジトリって、ざっくり言うと👇
- DB操作の道具箱じゃなくて🧰
- **集約(Aggregate)を出し入れする“専用の棚”**📚
なのに、Genericsで共通化しすぎると……
✅ 設計の中心が「ドメイン」じゃなく「CRUD」になる ✅ メソッド名が「業務の言葉」じゃなく「技術の言葉」になる ✅ だんだん “DBを触る都合”でアプリが形作られる ✅ そして最終的に Application層がクエリ地獄になる😵💫
「DDDやってるのに、結局DB中心じゃん…🥲」ってなりがちです。
2. ありがちな「共通リポジトリ」例😺(そして危険)
たとえばこういうやつ👇(気持ちは分かる✨)
public interface IRepository<TEntity, TId>
{
Task<TEntity?> GetByIdAsync(TId id, CancellationToken ct = default);
Task<List<TEntity>> GetAllAsync(CancellationToken ct = default);
Task AddAsync(TEntity entity, CancellationToken ct = default);
Task UpdateAsync(TEntity entity, CancellationToken ct = default);
Task DeleteAsync(TEntity entity, CancellationToken ct = default);
Task<List<TEntity>> FindAsync(
Expression<Func<TEntity, bool>> predicate,
CancellationToken ct = default);
}
一見便利!🤩 でもDDD的には、ここがツラいポイント👇
❌ 罠ポイント①:「業務の言葉」が消える🫥
FindAsync(predicate) って、業務的には何?🤔
例えば注文(Order)なら、本当はこう言いたいはず👇
- 「未払いの注文を探す」💸
- 「期限切れの予約を探す」⏰
- 「再送が必要なメールを探す」📩
でも共通化すると全部 Find になって、
ユビキタス言語(業務の言葉)がインターフェースから蒸発します🫠
❌ 罠ポイント②:Expression が “インフラ臭” を持ち込む🤢
Expression<Func<...>> はEF Coreと相性いいけど、DDD目線だと👇
- ドメイン層のインターフェースが 「検索の仕方(技術)」を知ってしまう😵💫
- 将来、DBや検索方式が変わると インターフェースが巻き添えになる⚡
❌ 罠ポイント③:Application層が「クエリ職人」になる👩🍳
共通リポジトリがあると、ユースケース側がだんだんこうなる👇
var orders = await _repo.FindAsync(
o => o.Status == OrderStatus.Unpaid
&& o.CreatedAt < DateTimeOffset.UtcNow.AddDays(-7)
&& o.TotalAmount > 0,
ct);
これ、ドメイン知識(未払い、7日、金額…)がApplication層に漏れてるんですよね🥲 増えてくると「どこに何のルールがあるの?」って迷子になります🌀
3. DDDっぽい「正解寄り」✅:集約ごとのリポジトリにする📦
DDDではふつう、集約ルートごとに専用リポジトリを作ります✨ (“注文の棚📚” “顧客の棚📚” みたいなイメージ)
例:Order 集約のリポジトリ
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
Task AddAsync(Order order, CancellationToken ct = default);
// 👇 業務の言葉でクエリできるのが強い!
Task<bool> ExistsOrderNumberAsync(OrderNumber number, CancellationToken ct = default);
Task<List<Order>> FindUnpaidOlderThanAsync(TimeSpan age, CancellationToken ct = default);
}
この形の良いところ👇
- メソッド名が 業務そのもの🗣️✨
- “やりたいこと”が読める📖
- ルールの置き場所がブレにくい🧭
- AIに指示もしやすい🤖💡(「未払い古い注文を探すメソッド作って」で通る)
4. 「でも共通化したい…🥺」→ やっていい共通化/ダメな共通化
✅ やっていい共通化(裏側だけ)🧊
インターフェースは専用のまま、実装の中で共通化するのはアリ🙆♀️✨ 例えば Infrastructure にだけ、こっそり置く👇
RepositoryBase<TEntity>(EF Coreの共通処理まとめ)DbContext周りの共通処理SaveChangesの取り回し
つまり、外に見せる顔(インターフェース)はドメイン語、 中身(実装)は共通化でOKです😎👍
❌ やりすぎ共通化(表に出す共通IRepository)🧨
IRepository<T>をドメイン層に置くFind(Expression<...>)をドメイン層の契約にするGetAll()が普通に呼べる(←危険⚠️ “全部取る” が癖になる)
このへんは、DDD初心者ほどハマりやすいです😂🪤
5. 「共通Repoが欲しい!」の本当の気持ちを分解しよう🧠✨
共通化したくなる理由って、だいたいこれ👇
- 似たようなCRUDが多い😵
- 毎回同じ実装が面倒🥺
- AIに作らせたら速いから、構造もAIに寄せたくなる🤖
ここでおすすめの考え方👇
✅ “繰り返しコード”はAIで解決する ✅ “設計の言葉”は共通化で消さない
つまり、共通化でラクをするんじゃなくて、AIでラクをするのが今っぽいです✨🤝
6. AIに頼むときの「良い指示」テンプレ🤖📝
Copilot/Codexにこう投げると事故りにくいです👇
Order集約のリポジトリIOrderRepositoryを作って。
DDD前提で、CRUDっぽい汎用メソッド名は避けて。
ユビキタス言語で、業務の意図が分かるメソッド名にして。
Expression<Func<...>> や IQueryable はインターフェースに出さないで。
必要なメソッドは
- GetByIdAsync
- AddAsync
- ExistsOrderNumberAsync
- FindUnpaidOlderThanAsync
ポイントは「ダメな形を先に禁止する」ことです😆🚫
7. まとめ🎀(今日のゴール)
- Genericsで
IRepository<T>を作ると、DDDがCRUDに寄りやすい⚠️ - インターフェースは集約ごとに作るのが基本📦✨
- 共通化したいなら、実装の内部(Infrastructure)でこっそりやる🧊
- “ラク”は共通化で取るより、AIで取る🤖💨
ちょい演習🎓✨(10〜15分)
あなたの適当な集約(例:User, Task, Order なんでもOK)で👇
- まず
IRepository<T>っぽい案をAIに作らせる🤖 - その後で「DDD前提で、集約専用リポジトリに直して」と頼む🛠️
- 最後に、メソッド名が業務の言葉になってるかチェック✅🗣️
「Find って何探してるの?🤔」って聞いて答えられなかったら、だいたい改善ポイントあります😆👍
次の章(第49章)は「Factory」🏭✨ “生成が複雑な子を、ちゃんと生ませる方法”やっていこうね〜😊🎉