Supple Designとは何か
Eric Evansは『Domain-Driven Design』第10章で、ドメインモデルが硬直化し変更困難になる問題に対する処方箋として Supple Design(しなやかな設計)を提示しました。 戦術的設計のビルディングブロック(エンティティ、値オブジェクト、集約など)を「何を作るか」の語彙とするなら、 Supple Designは「どう作るか」の品質基準です。
目指すのは、利用する側にとって意図が明確で、変更する側にとって安全に修正できるコードです。 Supple Designの7つのパターンは単独で機能するのではなく、相互に補強し合ってコードの柔軟性を高めます。
graph TB
subgraph layer3[統合層 — 組み合わせを強化]
DD[Declarative Design\n宣言的設計]
CO[Closure of Operations\n操作の閉包]
end
subgraph layer2[構造層 — 依存を整理]
CC[Conceptual Contours\n概念の輪郭]
SC[Standalone Classes\n独立したクラス]
end
subgraph layer1[理解層 — 意図を明確に]
IRI[Intention-Revealing\nInterfaces]
SEF[Side-Effect-Free\nFunctions]
AS[Assertions\n表明]
end
IRI --> CC
SEF --> CO
AS --> DD
CC --> DD
SC --> CO
style layer1 fill:#1e1e2e,stroke:#3b82f6,color:#94a3b8
style layer2 fill:#1e1e2e,stroke:#8b5cf6,color:#94a3b8
style layer3 fill:#1e1e2e,stroke:#f97316,color:#94a3b8
style IRI fill:#3b82f6,stroke:#1d4ed8,color:#fff
style SEF fill:#3b82f6,stroke:#1d4ed8,color:#fff
style AS fill:#3b82f6,stroke:#1d4ed8,color:#fff
style CC fill:#8b5cf6,stroke:#6d28d9,color:#fff
style SC fill:#8b5cf6,stroke:#6d28d9,color:#fff
style DD fill:#f97316,stroke:#ea580c,color:#fff
style CO fill:#f97316,stroke:#ea580c,color:#fff1. Intention-Revealing Interfaces(意図を表すインターフェース)
メソッド名やクラス名が「何をするか」ではなく「なぜ呼ぶのか」を伝えるように命名するパターンです。 利用者は実装の詳細を読まなくても、名前だけでそのコンポーネントの意図と効果を理解できるべきです。
// 悪い例: 実装の詳細が名前に漏れている
class Paint {
// v1, v2が何を意味するか分からない
mix(v1: number, v2: number): Paint { ... }
}
// 良い例: 意図が名前から読み取れる
class Paint {
mixWith(other: Paint, ratio: MixRatio): Paint { ... }
}
// ドメインの意図がさらに明確な例
class Policy {
// 「何をするか」ではなく「なぜ使うか」が分かる
isEligibleFor(claim: InsuranceClaim): boolean { ... }
calculatePremiumFor(driver: Driver): Money { ... }
} 2. Side-Effect-Free Functions(副作用のない関数)
副作用のない関数(純粋関数)は、引数のみに依存し、外部状態を変更せず、同じ入力に対して常に同じ出力を返します。 ドメインロジックの大部分を副作用のない関数として設計することで、テスト容易性・推論容易性・組み合わせ可能性が飛躍的に高まります。
DDDにおける値オブジェクトは、この原則を体現するのに最適な場所です。 値オブジェクトの操作は常に新しいインスタンスを返し、元のインスタンスを変更しません。
// 値オブジェクトの副作用のない操作
class DateRange {
constructor(
readonly start: Date,
readonly end: Date
) { Object.freeze(this); }
// 副作用なし: 新しいDateRangeを返す
extendBy(days: number): DateRange {
const newEnd = addDays(this.end, days);
return new DateRange(this.start, newEnd);
}
// 副作用なし: booleanを返すだけ
overlaps(other: DateRange): boolean {
return this.start < other.end && other.start < this.end;
}
} 3. Assertions(表明)
表明(Assertions)は、操作の事前条件・事後条件・不変条件をコード上で明示するパターンです。 コメントではなくコードとして表現することで、開発者は操作の契約を正確に把握でき、 実装の変更時にも契約の遵守を検証できます。
class BankAccount {
private balance: Money;
// 事前条件: 引き出し額 > 0、残高 >= 引き出し額
// 事後条件: 残高が引き出し額分だけ減少
// 不変条件: 残高は常に0以上
withdraw(amount: Money): void {
// 事前条件の表明
if (amount.isNegativeOrZero()) {
throw new Error('引き出し額は正の値である必要があります');
}
if (this.balance.isLessThan(amount)) {
throw new Error('残高が不足しています');
}
this.balance = this.balance.subtract(amount);
// 不変条件の表明
assert(this.balance.isNonNegative(),
'残高が負になりました — 不変条件違反');
}
} 4. Conceptual Contours(概念の輪郭)
概念の輪郭とは、ドメインの自然な分割線に沿ってコードを分割するパターンです。 技術的な都合(レイヤー分割、機能分割)ではなく、ドメインの概念が「変化する単位」でクラスやメソッドを区切るのがポイントです。
うまく分割できていれば、変更が必要なときに関連する部分だけが変わり、無関係な部分に影響しません。 逆に分割が不適切だと、1つの変更が複数の場所に波及する「散弾銃手術(Shotgun Surgery)」が発生します。
5. Standalone Classes(独立したクラス)
独立したクラスは、他のクラスへの依存を極力減らし、単独で理解・テスト・再利用できるクラスを目指すパターンです。 依存が少ないほど認知負荷が下がり、変更の影響範囲も小さくなります。
特に値オブジェクトは、Standalone Classの最も良い実践例です。
Money、EmailAddress、DateRangeのような値オブジェクトは、
他のドメインオブジェクトに依存せず完全に独立して動作します。
6. Closure of Operations(操作の閉包)
操作の閉包とは、ある型に対する操作の戻り値が同じ型になる設計パターンです。 数学における「閉包(closure)」の概念に由来します。整数の加算が常に整数を返すように、 ドメインオブジェクトの操作が同じ型のドメインオブジェクトを返すことで、 操作をチェーン(連鎖)して使えるようになります。
class Money {
// Money + Money → Money(閉包性)
add(other: Money): Money {
return Money.create(this.amount + other.amount, this.currency);
}
// Money * number → Money(引数の型は異なるが戻り値はMoney)
multiply(factor: number): Money {
return Money.create(this.amount * factor, this.currency);
}
}
// 閉包性により自然にチェーンできる
const total = basePrice
.multiply(quantity)
.add(shippingFee)
.add(tax); 7. Declarative Design(宣言的設計)
宣言的設計は、「どう実行するか(how)」ではなく「何を達成したいか(what)」を表現するスタイルです。 手続き的な制御フローを書く代わりに、ルールや制約をオブジェクトとして宣言し、 フレームワークやエンジンにその実行を委ねます。
DDDにおける宣言的設計の代表例がSpecificationパターンです。
Specificationパターン — 宣言的設計の好例
Specificationパターンは、ビジネスルール(条件判定)を個別のオブジェクトとして切り出し、
and、or、notで組み合わせ可能にする設計です。
条件ロジックがif文の入れ子として散らばる代わりに、名前付きのルールとして宣言的に表現できます。
// Specificationの基底
interface Specification<T> {
isSatisfiedBy(candidate: T): boolean;
and(other: Specification<T>): Specification<T>;
or(other: Specification<T>): Specification<T>;
not(): Specification<T>;
}
// 個別のビジネスルールをSpecificationとして宣言
class IsAdult implements Specification<Customer> {
isSatisfiedBy(c: Customer) { return c.age >= 18; }
}
class HasGoodCredit implements Specification<Customer> {
isSatisfiedBy(c: Customer) { return c.creditScore >= 700; }
}
// 宣言的に組み合わせる
const eligibleForLoan = new IsAdult()
.and(new HasGoodCredit());
// 使う側は「何を判定するか」だけを読む
if (eligibleForLoan.isSatisfiedBy(customer)) {
approveLoan(customer);
} パターン間の関係と3層構造
7つのパターンは独立して存在するのではなく、3つの層として段階的にコードの品質を高めます。
| 層 | パターン | 効果 |
|---|---|---|
| 理解層 | Intention-Revealing Interfaces | 名前だけで意図が伝わる |
| 理解層 | Side-Effect-Free Functions | 安全に呼び出せる、テストが容易 |
| 理解層 | Assertions | 契約(事前・事後・不変条件)が明確 |
| 構造層 | Conceptual Contours | ドメインの自然な境界で分割 |
| 構造層 | Standalone Classes | 依存を最小化し認知負荷を下げる |
| 統合層 | Closure of Operations | 操作をチェーンして組み合わせ可能に |
| 統合層 | Declarative Design | ルールをオブジェクトとして宣言的に表現 |
DDD全体におけるSupple Designの位置づけ
Supple Designは戦術的設計のパターン(エンティティ、値オブジェクト、集約など)の品質を底上げする原則群です。
たとえば、値オブジェクトにSide-Effect-Free FunctionsとClosure of Operationsを適用すれば、
Moneyのように安全にチェーン可能なオブジェクトが生まれます。
集約のメソッドにIntention-Revealing InterfacesとAssertionsを適用すれば、
使う側が安心してメソッドを呼び出せるようになります。
理解度チェック
Supple Designの7パターンの3層構造で、「理解層」に含まれないパターンはどれですか?
キーボード: 1〜4 で選択、Enter で回答