第85章:Policy(方針)入門:条件→行動🧠➡️🏃♀️
この章はね、Specificationで「条件(真/偽)」をきれいにしたあとに、 **「じゃあ条件を満たしたら、何をするの?」**をスッキリ書くための道具が **Policy(方針)**だよ〜!🫶✨
1) 今日のゴール 🎯✨
- 「条件(Specification)」と「行動(Policy)」を分けて考えられる🧩
- ifが増えても、読むのがラクな形で「運用ルール」を書ける🌿
- Policyをドメインの言葉としてコードに残せる📘💛
2) Policyってなに?(超やさしく)🧠💡
✅ Specification:質問する人 🙋♀️
- 「この注文、学生割の対象?」
- 「この注文、合計が1200円以上?」 → 真/偽を返す(Yes/No)だけの係
Specificationは「ビジネスルールを組み合わせる」ためのパターンとしてDDD文脈でもよく使われるよ〜。(ウィキペディア)
✅ Policy:決める人 🧑⚖️➡️📝
- 「学生割の対象なら、10%割引を適用して、次回クーポン通知も予約する」 → **条件→行動(方針)**を表現する係
DDDの文脈でも、Policyは「ルールを明示的に分離する」目的で使われ、STRATEGY(戦略)と同じ発想として紹介されてるよ。(fabiofumarola.github.io)
3) なんで必要?(if地獄の未来👀⚠️)
たとえば「キャンペーン」が増えると…
- 学生割 🎓
- 平日限定 📅
- アプリ決済限定 📱
- 初回注文限定 🌟
- 雨の日ボーナス ☔
これをアプリ層で全部 if で書くと、読みづらい・直しづらい・漏れるの三重苦😵💫💥 だからこうするのが気持ちいい✨
- 条件はSpecification(質問)
- “どうするか”はPolicy(方針)
- 実際の副作用(通知送信など)はアプリ層(実行)
4) 章のメイン例題 ☕🧾:「学生・平日・合計1200円以上なら…」
方針(Policy): 🎓学生 かつ 📅平日 かつ 💴合計1200円以上 なら ➡️ 10%割引を適用して、📩 次回クーポン通知を予約する
ここで大事ポイント💡 Policyは「通知を送る」みたいな副作用を直接やらないで、 “やるべきこと(計画)”を返すのが扱いやすいよ〜🫶✨
5) 実装してみよう(最小セット)🧩🧠➡️🏃♀️
ここでは、Specificationは第82〜84章の資産がある前提で「最小の形」だけ置くね✨ (合成AND/ORは、今まで通り使う想定だよ〜!)
5-1. ドメイン:Specification(質問)🔎📄
// domain/specification/Specification.ts
export interface Specification<T> {
isSatisfiedBy(candidate: T): boolean;
}
export class AndSpecification<T> implements Specification<T> {
constructor(
private readonly left: Specification<T>,
private readonly right: Specification<T>,
) {}
isSatisfiedBy(candidate: T): boolean {
return this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate);
}
}
5-2. ドメイン:例題の型(Orderなど)☕🧾
// domain/order/Order.ts
export type OrderDayType = "weekday" | "weekend";
export class Money {
private constructor(private readonly yen: number) {}
static ofYen(yen: number): Money {
if (!Number.isInteger(yen) || yen < 0) throw new Error("Money must be a non-negative integer yen.");
return new Money(yen);
}
toYen(): number {
return this.yen;
}
isGte(other: Money): boolean {
return this.yen >= other.yen;
}
percentOff(rate: number): Money {
// rate: 0.10 = 10% off
if (!(rate > 0 && rate < 1)) throw new Error("rate must be between 0 and 1.");
const discount = Math.floor(this.yen * rate);
return Money.ofYen(discount);
}
minus(discount: Money): Money {
const v = this.yen - discount.yen;
if (v < 0) throw new Error("total cannot be negative.");
return Money.ofYen(v);
}
}
export class Customer {
constructor(
public readonly customerId: string,
public readonly isStudent: boolean,
) {}
}
export class Order {
private discount: Money = Money.ofYen(0);
constructor(
public readonly orderId: string,
public readonly customer: Customer,
public readonly dayType: OrderDayType,
private readonly subtotal: Money,
) {}
getSubtotal(): Money {
return this.subtotal;
}
getDiscount(): Money {
return this.discount;
}
getTotal(): Money {
return this.subtotal.minus(this.discount);
}
applyDiscount(discount: Money): void {
// 例:割引は小計を超えちゃダメ、とかもここで守れるよ✨
this.discount = discount;
}
}
5-3. ドメイン:今回のSpecificationたち(条件)🎓📅💴
// domain/promotion/specs.ts
import { Money, Order } from "../order/Order";
import { Specification } from "../specification/Specification";
export class StudentCustomerSpec implements Specification<Order> {
isSatisfiedBy(order: Order): boolean {
return order.customer.isStudent;
}
}
export class WeekdaySpec implements Specification<Order> {
isSatisfiedBy(order: Order): boolean {
return order.dayType === "weekday";
}
}
export class SubtotalAtLeastSpec implements Specification<Order> {
constructor(private readonly min: Money) {}
isSatisfiedBy(order: Order): boolean {
return order.getSubtotal().isGte(this.min);
}
}
5-4. ドメイン:Policy(方針)🧠➡️🏃♀️
「何をするべきか」を **Decision(決定)**として返すよ✨
// domain/policy/StudentWeekdayPromotionPolicy.ts
import { Money, Order } from "../order/Order";
import { AndSpecification } from "../specification/Specification";
import { StudentCustomerSpec, SubtotalAtLeastSpec, WeekdaySpec } from "../promotion/specs";
export type PromotionDecision =
| { kind: "none" }
| {
kind: "apply";
discount: Money;
shouldScheduleCouponNotification: boolean;
notificationMessage: string;
};
export class StudentWeekdayPromotionPolicy {
private readonly eligibleSpec: AndSpecification<Order>;
constructor() {
const isStudent = new StudentCustomerSpec();
const isWeekday = new WeekdaySpec();
const minSubtotal = new SubtotalAtLeastSpec(Money.ofYen(1200));
this.eligibleSpec = new AndSpecification(
new AndSpecification(isStudent, isWeekday),
minSubtotal,
);
}
decide(order: Order): PromotionDecision {
if (!this.eligibleSpec.isSatisfiedBy(order)) {
return { kind: "none" };
}
const discountRate = 0.10; // 10%
const discount = order.getSubtotal().percentOff(discountRate);
return {
kind: "apply",
discount,
shouldScheduleCouponNotification: true,
notificationMessage: "学生さん平日特典だよ🎓✨ 次回使えるクーポンを用意したよ〜!",
};
}
}
✅ ここがキレイポイント✨
- 条件はSpecificationで読める
- Policyは「決める」だけ
- 副作用は外(アプリ層)で実行しやすい
6) アプリ層で“実行”する(通知はここで)📨🧑🍳
// app/ApplyPromotionToOrder.ts
import { StudentWeekdayPromotionPolicy } from "../domain/policy/StudentWeekdayPromotionPolicy";
import { Order } from "../domain/order/Order";
export interface NotificationPort {
scheduleCoupon(customerId: string, message: string): Promise<void>;
}
export class ApplyPromotionToOrder {
constructor(
private readonly policy: StudentWeekdayPromotionPolicy,
private readonly notification: NotificationPort,
) {}
async execute(order: Order): Promise<void> {
const decision = this.policy.decide(order);
if (decision.kind === "none") return;
order.applyDiscount(decision.discount);
if (decision.shouldScheduleCouponNotification) {
await this.notification.scheduleCoupon(order.customer.customerId, decision.notificationMessage);
}
}
}
7) テスト(Vitestでサクッと)🧪✨
2026年頭時点でも、Vitestは継続的に更新されてて、直近は v4.x 系が出てるよ〜。(NPM) TypeScript自体も 5.9 系(npm上は 5.9.3 が “latest” 表示)になってる。(NPM)
// test/StudentWeekdayPromotionPolicy.test.ts
import { describe, it, expect } from "vitest";
import { Customer, Money, Order } from "../src/domain/order/Order";
import { StudentWeekdayPromotionPolicy } from "../src/domain/policy/StudentWeekdayPromotionPolicy";
const createOrder = (params: { isStudent: boolean; dayType: "weekday" | "weekend"; subtotalYen: number }) => {
const customer = new Customer("CUST-1", params.isStudent);
return new Order("ORDER-1", customer, params.dayType, Money.ofYen(params.subtotalYen));
};
describe("StudentWeekdayPromotionPolicy", () => {
it("学生×平日×小計1200円以上なら、10%割引+通知予約", () => {
const policy = new StudentWeekdayPromotionPolicy();
const order = createOrder({ isStudent: true, dayType: "weekday", subtotalYen: 2000 });
const decision = policy.decide(order);
expect(decision.kind).toBe("apply");
if (decision.kind === "apply") {
expect(decision.discount.toYen()).toBe(200); // 2000の10%
expect(decision.shouldScheduleCouponNotification).toBe(true);
}
});
it("学生じゃないなら対象外", () => {
const policy = new StudentWeekdayPromotionPolicy();
const order = createOrder({ isStudent: false, dayType: "weekday", subtotalYen: 2000 });
expect(policy.decide(order).kind).toBe("none");
});
it("休日なら対象外", () => {
const policy = new StudentWeekdayPromotionPolicy();
const order = createOrder({ isStudent: true, dayType: "weekend", subtotalYen: 2000 });
expect(policy.decide(order).kind).toBe("none");
});
it("小計が足りないなら対象外", () => {
const policy = new StudentWeekdayPromotionPolicy();
const order = createOrder({ isStudent: true, dayType: "weekday", subtotalYen: 1199 });
expect(policy.decide(order).kind).toBe("none");
});
});
8) よくある失敗あるある 😂⚠️
❌ Policyが“何でも屋”になる
- Policyの中でDB保存したり、API叩いたり、ログ出したり… → 「決める」じゃなく「全部やる」になって破綻しがち😇
✅ 対策:Policyは Decisionを返すだけに寄せる✨
❌ SpecificationとPolicyが混ざる
- 「isSatisfiedByの中で割引適用までやる」みたいなやつ → “質問”なのか“行動”なのか分からなくなる🌀
✅ 対策:
- Specification = 真/偽(質問)
- Policy = 行動方針(決める)
❌ ifの塊をPolicyに移しただけ
- 「Policyにしたけど中身がifだらけ」😵💫 ✅ 対策:条件はSpecificationに寄せて、Policyは読み物にする📘✨
9) AIの使いどころ(超効く)🤖💞
🪄 Policy候補を洗い出すプロンプト
- 「カフェ注文ドメインで “条件→行動” になっている運用ルールを10個出して。条件と行動を分けて書いて」
🪄 Decision設計を整えるプロンプト
- 「このPolicyが返すDecisionの型を、将来の追加に強い形にしたい。union型の案を3つ出して、メリデメも書いて」
🪄 テスト抜けを見つけるプロンプト
- 「このPolicyのテスト観点を網羅して。正常系/境界値/例外系/将来の追加に弱い点も含めて」
10) ミニ演習(やると一気に強くなる)🎮✨
演習A:方針を1個追加してみよ🧠➡️🏃♀️
- 「雨の日(RainyDaySpec)ならホットドリンクをおすすめ通知する」☔☕ → Policyは通知“計画”を返すだけにしてね✨
演習B:Policyを2段にしてみよ🪜
- 「複数キャンペーンがあるとき、どれを優先する?」 → PromotionPolicyを “合成” できる形にしてみる(Decisionをマージ)🧩
11) まとめ 🎀✨
- Specificationは「条件(質問)」🔎
- Policyは「条件→行動(方針)」🧠➡️🏃♀️
- Policyは 副作用を直接やらず、Decision(やること)を返すと超キレイ💎
- DDDでもPolicyはルールを明示的に分離するために使われ、Strategy的な考え方で語られるよ📘✨ (fabiofumarola.github.io)
次章へのつなぎ ⏰🧪✨
次の第86章は 時間(今)をテスト可能にするClock注入だよ〜! Policyって「期限」「曜日」「時間帯」と相性が良すぎるから、ここが揃うと一気に実務っぽくなる💘🔥
必要なら、この章の例をそのまま Clock対応版に進化させた形もセットで作れるよ〜!🥳💞