Skip to main content

第72章:InMemory Repositoryで先に進む🧪📦

1) この章でできるようになること🎯✨

  • DBがまだ無くても、PlaceOrder / PayOrder / FulfillOrder / GetOrder をちゃんと回せるようにする🎬☕💳
  • 「Repositoryはinterfaceがドメイン、実装がインフラ」をコードで体感する📚🧠
  • InMemory特有の事故(共有状態、参照漏れ、テストが不安定…)を先に潰す🧯😵‍💫

2) InMemory Repositoryってなに?なんで便利?🤔💡

Map(メモリ上の辞書)に保存するRepositoryです🗂️✨ ポイントはこれ👇

  • 速い(DB待ちゼロ)🚀
  • 実装が軽い(MapでOK)🪶
  • ユースケースの配線が先に完成する(ここが超大事!)🎉
  • ✅ 後でDB版Repositoryに差し替えても、アプリ層がほぼ変わらない🔁

つまり「DDDの形を守ったまま、先に前進できる道具」って感じです🛼🌈


3) DDD的に“置き場所”が超重要📦🧭

ここ、守れると一気にDDDっぽくなります🥳

  • 🏠 domain:Repositoryの「約束(interface)」だけ置く
  • 🏭 infra:その約束を満たす「実装」を置く(InMemory / DB / API…)

こうしておくと、保存先が変わってもドメインは平和です🕊️✨


4) 先に結論:InMemoryでやりがちな事故トップ3😂⚠️

事故①:テスト間でデータが残って壊れる🧨

  • 原因:Repositoryを使い回してる(共有状態)
  • 対策:テストごとにnewする or clearする🧼

事故②:Repositoryが返したOrderを外から弄れてしまう🧟‍♀️

  • 原因:Mapに「Orderオブジェクトそのもの」を保存して、その参照を返してる
  • 対策:スナップショット(プリミティブ)で保存して、取り出す時に復元する📸✨

事故③:後でDB版に差し替えるとasyncの差で死ぬ😇

  • 原因:InMemoryだけ同期で作ってしまった
  • 対策:最初からPromiseに合わせる(save/findはasync)⏳✅

5) 実装方針:スナップショット方式が最強📸🛡️

Mapの中には Orderそのもの じゃなくて、こういう「JSONにできる形」を入れます👇

  • ✅ 参照漏れ(外部から勝手に変更)を防げる
  • ✅ 将来DBに保存する形に近い
  • ✅ テストが安定する

6) コード:domain側(interfaceとスナップショット)📚🧊

OrderRepository(domain)

// src/domain/order/OrderRepository.ts
import { Order } from "./Order";
import { OrderId } from "./OrderId";

export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: OrderId): Promise<Order | null>;
}

OrderSnapshot(domain寄りでOK)

「復元できる最低限の形」だけにします📦✨

// src/domain/order/OrderSnapshot.ts
export type OrderStatus = "Draft" | "Confirmed" | "Paid" | "Fulfilled" | "Canceled";

export type OrderLineSnapshot = {
menuItemId: string;
quantity: number;
unitPriceYen: number;
};

export type OrderSnapshot = {
id: string;
status: OrderStatus;
lines: OrderLineSnapshot[];
createdAtIso: string;
};

Order側:toSnapshot / fromSnapshot(復元の入口)🚪✨

Factoryは第74章で本格的にやるけど、ここでは「復元の静的メソッド」で十分です🙆‍♀️💖

// src/domain/order/Order.ts
import { OrderId } from "./OrderId";
import { OrderSnapshot, OrderStatus } from "./OrderSnapshot";

export class Order {
private constructor(
private readonly _id: OrderId,
private _status: OrderStatus,
private _lines: ReadonlyArray<{ menuItemId: string; quantity: number; unitPriceYen: number }>,
private readonly _createdAtIso: string,
) {}

get id(): OrderId {
return this._id;
}

get status(): OrderStatus {
return this._status;
}

// 例:とりあえずスナップショットに落とす
toSnapshot(): OrderSnapshot {
return {
id: this._id.value,
status: this._status,
lines: this._lines.map((x) => ({ ...x })),
createdAtIso: this._createdAtIso,
};
}

static fromSnapshot(s: OrderSnapshot): Order {
return new Order(
OrderId.fromString(s.id),
s.status,
s.lines.map((x) => ({ ...x })),
s.createdAtIso,
);
}

// 例:最小の生成(本格的な生成ルールは別章で強化)
static createNew(id: OrderId, nowIso: string): Order {
return new Order(id, "Draft", [], nowIso);
}
}

(補足)TypeScriptは最近、Node向けの挙動を安定させるためのオプションを拡充していて、モジュール解決の「安定点」を選びやすくなってます。(typescriptlang.org)


7) コード:infra側(InMemory実装)🏭🗺️

Mapの中には OrderSnapshot を保存します📸✨

// src/infra/order/InMemoryOrderRepository.ts
import { OrderRepository } from "../../domain/order/OrderRepository";
import { Order } from "../../domain/order/Order";
import { OrderId } from "../../domain/order/OrderId";
import { OrderSnapshot } from "../../domain/order/OrderSnapshot";

export class InMemoryOrderRepository implements OrderRepository {
private readonly store = new Map<string, OrderSnapshot>();

async save(order: Order): Promise<void> {
const snapshot = order.toSnapshot();
// 安全のために「コピーして保存」しておく(参照事故をさらに減らす)
this.store.set(snapshot.id, {
...snapshot,
lines: snapshot.lines.map((x) => ({ ...x })),
});
}

async findById(id: OrderId): Promise<Order | null> {
const snapshot = this.store.get(id.value);
if (!snapshot) return null;

// 取り出しもコピーして復元(外部からstoreを汚さない)
const cloned: OrderSnapshot = {
...snapshot,
lines: snapshot.lines.map((x) => ({ ...x })),
};
return Order.fromSnapshot(cloned);
}

// テスト用(domainのinterfaceには入れない)
clearForTest(): void {
this.store.clear();
}
}

8) ユースケースに差すとこうなる🎬🔌

「保存先がInMemoryかどうか」をユースケースが知らないのが理想です💖

// src/app/order/GetOrderService.ts
import { OrderRepository } from "../../domain/order/OrderRepository";
import { OrderId } from "../../domain/order/OrderId";

export type GetOrderResult = {
id: string;
status: string;
lineCount: number;
};

export class GetOrderService {
constructor(private readonly orderRepo: OrderRepository) {}

async execute(orderId: string): Promise<GetOrderResult | null> {
const order = await this.orderRepo.findById(OrderId.fromString(orderId));
if (!order) return null;

const snap = order.toSnapshot();
return {
id: snap.id,
status: snap.status,
lineCount: snap.lines.length,
};
}
}

9) テスト:Vitestで“速い安心”を作る🧪💨

Vitestは4系が出ていて(2025年後半に4.0告知)、直近だと4.1のベータも動いてます。(vitest.dev) (テストが速いと、DDD学習がほんと捗ります🏃‍♀️✨)

// src/infra/order/InMemoryOrderRepository.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { InMemoryOrderRepository } from "./InMemoryOrderRepository";
import { Order } from "../../domain/order/Order";
import { OrderId } from "../../domain/order/OrderId";

describe("InMemoryOrderRepository", () => {
let repo: InMemoryOrderRepository;

beforeEach(() => {
repo = new InMemoryOrderRepository(); // ✅ テストごとにnew(共有状態を潰す)
});

it("saveしたOrderをfindByIdで取れる😊", async () => {
const id = OrderId.newId();
const order = Order.createNew(id, new Date().toISOString());

await repo.save(order);
const loaded = await repo.findById(id);

expect(loaded).not.toBeNull();
expect(loaded!.id.value).toBe(id.value);
expect(loaded!.status).toBe("Draft");
});

it("取り出したOrderを外で変更しても、内部ストアが汚れない🛡️", async () => {
const id = OrderId.newId();
const order = Order.createNew(id, new Date().toISOString());

await repo.save(order);

const loaded1 = await repo.findById(id);
const loaded2 = await repo.findById(id);

// 参照が同じオブジェクトになってないこと(最低限の安全)
expect(loaded1).not.toBe(loaded2);
expect(loaded1!.toSnapshot()).toEqual(loaded2!.toSnapshot());
});
});

10) Node/TSの“いま”をちょい押さえ(地味に効く)🧠🪄

  • Nodeは「Current」と「Active LTS」が並行で進むので、安定運用寄りならLTSが選ばれがちです📌(nodejs.org)
  • 最近のNodeリリース状況(Current/LTSの更新)も公式にまとまってます📣(nodejs.org)
  • TypeScript側もNode向けオプションが整理されてきて、挙動の“固定点”が作りやすくなってます🧷(typescriptlang.org)

このへんを軽く意識しておくと、後で「モジュール解決が環境で違う…😭」みたいな事故が減ります✨


11) ありがちなNG例(やらないで〜!)🙅‍♀️💦

NG:OrderオブジェクトをそのままMapに入れる

// これ、外から勝手に書き換えられて事故ります😭
this.store.set(order.id.value, order);
return this.store.get(id.value) ?? null;

✅ さっきの「スナップショット保存→復元」が安全でしたね📸🛡️


12) AIの使いどころ(この章は相性よすぎ)🤖💞

コピペで使えるプロンプト置いとくね🧁✨

  • 🧠 設計レビュー用

    • 「InMemory Repositoryの実装で、参照漏れ・共有状態・テスト不安定化の可能性をチェックして。危険箇所と改善案を出して。」
  • 🧰 コード骨格生成(中身は自分で確認)

    • 「OrderRepository(save/findById)と、OrderSnapshot保存方式のInMemory実装の雛形をTypeScriptで。storeはMap、戻りはOrder.fromSnapshotで。」
  • 🧪 テスト観点追加

    • 「InMemory Repositoryのテスト観点を“失敗しやすい順”に10個。特に参照漏れとテスト間の状態混入を厚めに。」

13) ミニ演習(5〜15分)🎮🍬

演習A:GetOrderの表示項目を増やす📄✨

  • lineCountだけじゃなく、statusや合計金額(あれば)もDTOに入れてみよう💪

演習B:clearForTestを使わずに安定させる🧼

  • beforeEachでrepoをnewする方式に統一して、「共有状態ゼロ」にしよう🙌

演習C:存在しないIDの挙動を固める👻

  • findByIdがnullを返すのはOK?例外にする? → チーム方針っぽく文章で決めてみよう✍️✨

14) まとめ🎀✨

  • InMemory Repositoryは「DBが無い時期の最強ブースター」🚀
  • ただし事故りやすいので、スナップショット保存方式が超おすすめ📸🛡️
  • interface(domain)と実装(infra)を分けると、差し替えが気持ちいい🔁🎉

次は第73章で「Repositoryの粒度=集約単位」をさらにカチッと決めて、太りすぎを防ぎます📦✨