第71章:Repository入門:保存・取得の抽象化📚
1) 今日のゴール🎯🌸
この章が終わったら、こうなります👇
- Repositoryが「何のための道具」か説明できる🗣️✨
- domain側に
interfaceを置く理由がわかる📦🧠 OrderRepositoryのメソッド粒度を、ユースケースから逆算して決められる🔎🧾- Application Serviceが DB/Map/外部保存に触れずに注文を保存・取得できるようになる🎬🛡️
2) Repositoryってなに?(一言で)🧠📚
Repositoryはね…
**「集約(例:Order)を、保存・取得する“窓口”を1つにまとめたもの」**だよ💡✨ しかもポイントは、窓口の形(interface)はdomain側に置くこと!
3) Repositoryがない世界、だいたい地獄😵💫🪦
Repositoryがないと、よくこうなる👇
- Application Serviceが
MapやDBを直に触る - すると、いつの間にか **ユースケース手順の中に「保存の都合」や「検索の都合」**が混ざる
- さらに、domainがDB都合に引っぱられて汚れていく…😇
DDD(戦術DDD)の理想はこれ👇
- domain:ルール(不変条件・状態遷移)を守る🏯🔒
- app:手順を回す🎬
- infra:保存や通信の現実を担当する🧰
4) どこに置く?(超大事!)📦🧭
Repositoryはこう分けます👇
- domain:
OrderRepositoryinterface(「こう使いたい!」の希望)🌱 - infra:
OrderRepositoryの 実装(「実際どう保存するか」)🧰
依存の向きはこう👇(矢印の先を“知ってる”)
app → domain
infra → domain
domain は app/infra を知らない
これが「差し替え可能」になるカギ🔑✨ (DBを変えてもdomainは無傷〜!💃)
ちなみに、2026時点のNodeは「v24がActive LTS」になってるので、現実の実装(infra側)をあとで足していくのもしやすいよ🪟✨ (nodejs.org)
5) メソッド粒度の決め方🧩🔎(ここが腕の見せどころ!)
Repositoryのメソッドは、ユースケースから逆算して決めるのがコツ💡
いま私たちの例題ユースケースは(第70章までで)だいたい👇
- PlaceOrder:新規作成して保存
- PayOrder:注文を取得して支払い済みにして保存
- FulfillOrder:注文を取得して提供済みにして保存
- GetOrder:注文を取得して表示用に返す
だから最小セットはこうなりがち👇
save(order)findById(orderId)
「検索条件が増えそうだから」といって、いきなり
findByStatusfindByCustomersearch(...)みたいに増やすと、太りやすいよ😂💦(必要になったら足すでOK!)
6) domainに置く:OrderRepository interface(例)🪪📚
※ この章では「形」を作るのが目的!実装(Mapで保存とか)は次章でやるよ〜🧪📦
// domain/order/OrderRepository.ts
import { Order } from "./Order";
import { OrderId } from "./OrderId";
export interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
}
戻り値は null?それとも Result型?⚖️🎀
ここはチーム方針でOKだけど、超入門ならまずは👇がラク✨
- 見つからない →
null - ユースケース側で「なかったらエラー」にする
Result型にすると表現力は上がるけど、最初は型が増えて混乱しやすいかも😵💫 (第87〜89章のエラー設計の章で、もっとキレイにできるよ🧯✨)
7) appが使う:Application ServiceはRepositoryだけ知る🎬🛡️
「PayOrder」を超ざっくりで書くとこんな感じ👇
// app/PayOrderService.ts
import { OrderRepository } from "../domain/order/OrderRepository";
import { OrderId } from "../domain/order/OrderId";
export class PayOrderService {
constructor(private readonly orderRepo: OrderRepository) {}
async execute(input: { orderId: string }) {
const orderId = OrderId.fromString(input.orderId);
const order = await this.orderRepo.findById(orderId);
if (!order) {
// ここは「アプリ層の責務」でOK(見つからない → どうする?)
throw new Error("注文が見つかりませんでした🥲");
}
order.pay(); // ← domainのルールを動かす✨
await this.orderRepo.save(order); // ← 保存方法は知らない💾
}
}
ここが気持ちいいポイント😍✨
- appは「取得して→ドメイン操作→保存」しかしてない
- DBだろうがMapだろうが、どうでもいい(Repositoryが吸収!)
8) 「差し替えできる」ってどう嬉しいの?🔁🎉
Repositoryがあると…
- テストでは
InMemoryOrderRepositoryに差し替え🧪 - 本番では
DbOrderRepositoryに差し替え💾 - しかも、app/domain側のコードはほぼ変えない✨
これ、成長しても崩れにくい設計の“芯”だよ🏯💖
テスト周りは、2025年後半にVitest 4.0が出ていて(4系の流れが強い)、軽く速く回しやすいのも追い風〜🏃♀️💨 (vitest.dev) (Jestも安定版は30系だよ📌)(jestjs.io)
9) ありがち事故集😂⚠️(先に踏み抜きポイントを潰す!)
❌ 事故1:domainに「DBっぽい型」が漏れる
OrderRepository.save(orderRecord: OrderRow)みたいに「保存用の形」をdomainに入れない🙅♀️ → domainは **domainの型(Order)**で完結させよう✨
❌ 事故2:Repositoryが“何でも屋検索サービス”になる
searchOrders(filter: any)とかは危険🍄 → まずはユースケース最小で!
❌ 事故3:Repositoryが集約ルート以外も保存し始める
- LineItem単体を保存しだすと、集約ルールが崩れやすい😱 → Repositoryは基本「集約ルート単位」📦(第73章で深掘り!)
10) AI(Copilot/Codex)で爆速にするプロンプト集🤖💨📝
そのままコピペでOK系👇✨
✅ interfaceの骨格を出してもらう
- 「Order集約のRepository interfaceをTypeScriptで提案して。ユースケースは PlaceOrder/PayOrder/FulfillOrder/GetOrder。最小のメソッドに絞って。戻り値はPromiseで。」
✅ 粒度レビューしてもらう
- 「このRepositoryメソッド一覧、DDDの観点で太りすぎてない?不要なものを指摘して、削った最小案を出して。」
✅ 依存の向きレビュー
- 「domainがinfraを参照してしまっているimportがないか、チェック観点をリスト化して。」
11) 章末ミニ演習🎓🍰
演習A(やさしめ)🧁
OrderRepository に 「findById / save」以外を追加したくなった理由を3つ書いてみて✍️
→ その理由、本当に「今必要」?それとも「未来の不安」?🫣
演習B(ちょい実戦)🍩
MenuRepository を同じ方針で設計してみよう👇
- domainにinterface
- 最小メソッドだけ
- 検索メソッドを増やしすぎない
12) 理解チェック(3問)✅🌟
- Repositoryのinterfaceをdomain側に置くのはなぜ?(一言で)
findByIdがnullを返す設計のとき、「見つからない場合の扱い」はどの層の責務?- Repositoryメソッドが太り始めたときの“危険サイン”を1つ言ってみて👀
まとめ🎀✨
Repositoryは「保存・取得の窓口」を作って、domainを現実(DB)から守るバリアだよ🛡️💖
この章で「形(interface)」ができたから、次章で InMemory 実装して 差し替え体験しようね🧪📦🎉
(ちなみに、TypeScriptの公式リリースノートは2026年2月時点で5.9系が更新されてて、tsc --init まわりの改善なども入ってるよ🧡)(typescriptlang.org)