第18章:LSPの解決:継承より合成(合体だ!)🧱🤝
この章はね、**「継承で“それっぽく”作ったら、あとで爆発した…💥」**を回避する回だよ😊 ポイントはひとつ!
✅ “〜である(is-a)” じゃなくて “〜を持つ(has-a)” で組み立てる(=合成 / Composition) 🧩✨

1. この章でできるようになること🎯✨
- **LSP違反を起こしやすい“危険な継承”**を見抜ける👀⚠️
- 継承で作った設計を、**合成(部品の組み合わせ)**に直せる🔧🧱
- 「差し替えても壊れない」形に寄せる考え方が身につく🛡️😊
2. まず確認:なんで継承がLSPを壊しやすいの?😵💫
LSPって「親として使えるなら、子も同じように使えなきゃダメ」だよね🔁✨
ところが継承って、こうなりがち👇
- 親クラスが「できる」前提で持ってる機能を
- 子クラスが「やっぱ無理🥺」って 例外投げたり / 無視したりする
これ、置換できてないからLSP違反💥
3. 例題:注文キャンセルでLSPが崩れるやつ📦💣
「注文(Order)はキャンセルできるよね?」って気持ちで、こう書いちゃう例👇
// ❌ ありがちな継承設計(LSPが割れやすい)
class Order {
constructor(public readonly id: string) {}
cancel() {
// キャンセル処理(在庫戻す、返金する、など)
console.log(`[${this.id}] canceled`);
}
}
class PreparedOrder extends Order {
override cancel() {
// 😇 「調理済みだからキャンセル不可!」という現実…
throw new Error("Prepared order cannot be canceled.");
}
}
function cancelOrder(order: Order) {
// 「Orderならキャンセルできる」前提で呼んでる
order.cancel();
}
cancelOrder(new Order("A-001")); // ✅ OK
cancelOrder(new PreparedOrder("B-002")); // 💥 実行時に例外!
はい、これが LSP違反あるある📛
cancelOrder は Order を受け取ってるのに、子を入れたら壊れる…😇
4. 解決の基本:継承をやめて「キャンセルできるか」を部品にする🧱🤝
ここで出番なのが 合成(Composition)!✨ 注文そのものを継承で増やすんじゃなくて、
✅ 注文(Order)が“キャンセルのルール”を持つ(has-a) ✅ ルールを差し替える(Strategy)
って感じにするよ🎯
5. 解決策A:Strategyで「キャンセル方針」を合成する🎛️✨
5-1. まず “方針” の口(インターフェース)を作る🧩
type CancelResult =
| { ok: true }
| { ok: false; reason: string };
interface CancellationPolicy {
cancel(orderId: string): CancelResult;
}
5-2. 「キャンセルOK」「キャンセルNG」の部品を作る🧱
class AlwaysCancellable implements CancellationPolicy {
cancel(orderId: string): CancelResult {
console.log(`[${orderId}] canceled ✅`);
return { ok: true };
}
}
class NeverCancellable implements CancellationPolicy {
constructor(private readonly reason: string) {}
cancel(orderId: string): CancelResult {
console.log(`[${orderId}] cancel rejected ❌`);
return { ok: false, reason: this.reason };
}
}
5-3. Orderは “方針を持つ” だけ(合成!)🤝
class Order {
constructor(
public readonly id: string,
private readonly cancellationPolicy: CancellationPolicy,
) {}
cancel(): CancelResult {
return this.cancellationPolicy.cancel(this.id);
}
}
5-4. 使う側は “Orderを差し替えても壊れない” 🎉
function cancelOrder(order: Order) {
const result = order.cancel();
if (!result.ok) {
console.log(`キャンセルできなかったよ🥺 理由: ${result.reason}`);
}
}
const newOrder = new Order("A-001", new AlwaysCancellable());
const prepared = new Order("B-002", new NeverCancellable("調理済みだからだよ🍳"));
cancelOrder(newOrder); // ✅
cancelOrder(prepared); // ✅(例外で壊れない!)
ポイント💡
- “キャンセルできない注文” を 別クラスとして継承で表現しない
- 「できない」を 例外で表現しない(置換性が割れやすい💥)
- “ルール” を 差し替え可能な部品にしておく🧩✨
6. 解決策B:Stateで「注文の状態」を合成する🧠🧱(さらに強い)
キャンセル以外にも「支払い」「調理」「受け渡し」…って増えてくると、 Strategyより State(状態オブジェクト) がスッキリしやすいよ😊
type OrderActionResult =
| { ok: true }
| { ok: false; reason: string };
interface OrderState {
cancel(orderId: string): OrderActionResult;
}
class CreatedState implements OrderState {
cancel(orderId: string): OrderActionResult {
console.log(`[${orderId}] canceled ✅`);
return { ok: true };
}
}
class PreparedState implements OrderState {
cancel(orderId: string): OrderActionResult {
return { ok: false, reason: "調理済み🍳" };
}
}
class Order {
constructor(
public readonly id: string,
private state: OrderState,
) {}
cancel(): OrderActionResult {
return this.state.cancel(this.id);
}
// 状態遷移(例)
markPrepared() {
this.state = new PreparedState();
}
}
✅ 「状態が増える未来」にめちゃ強い💪✨ (しかも 継承の爆発を回避できるよ🔥)
7. 「継承 vs 合成」迷ったときの判断基準🧭✨
継承が向いてるのはこんな時👇
- 本当に “〜である(is-a)” が自然(例:
AdminUser is-a User) - 親の契約(やること・できること)が 子でも必ず守れる
- 親の設計が 今後ほぼ変わらない(ここ大事!)
合成が向いてるのはこんな時👇
- 「できる/できない」が状況で変わる(状態・制約・条件)😵
- 子クラスで 例外投げたくなってきた⚠️
overrideが増えて 親の意味が薄れてきた- “種類” より “振る舞い” を増やしたい(差し替えたい)🔁✨
8. AI(Copilot/Codex系)に手伝わせるコツ🤖🧠✨
合成に直すとき、AIにはこう頼むと強いよ👍
- 「この継承設計、LSP違反になりそう。Strategy(またはState)で合成に直して」
- 「例外を投げない設計にして、代わりにResult型で返して」
- 「呼び出し側の変更が最小になるように(API互換に寄せて)」
そして最後に必ずこれ👇
✅ “元のユースケースが壊れてないか” を自分で確認🔍 (AIはそれっぽく書くけど、契約がズレてることあるよ〜⚠️)
9. ミニ課題(手を動かそう)📝✨
課題1:LSP違反をわざと作る😈
Orderを継承したPreparedOrderでcancel()をthrowしてみて💥- 呼び出し側が壊れるのを確認👀
課題2:Strategyで直す🧱
-
CancellationPolicyを作ってAlwaysCancellableNeverCancellableを実装してみよう✨
課題3:Stateで拡張🧠
CreatedState / PaidState / PreparedStateを作ってcancel()の結果が状態で変わるようにしてみよう🔁
10. まとめ🎀✨(今日の合言葉!)
🎯 「継承で種類を増やす」より「合成で振る舞いを組み立てる」 これがLSPを守る近道だよ〜🧱🤝✨
最終チェックリスト✅
- 子クラスで
throwや「何もしない」が出てきてない?⚠️ - “できない” を 例外で表現してない?💥
- “振る舞い” を **部品(Strategy/State)**に分離できそう?🧩
- 差し替えても呼び出し側が壊れない?🔁🛡️
(ちょい最新メモ)いまのTypeScript周辺の動き🧠✨
- TypeScript は 5.9 系のリリースノートが公開されていて、
import deferなどの新要素が入ってるよ📦✨ (TypeScript) - そして Microsoft の公式ブログでは、TypeScript 6.0 を“橋渡し”として、7.0(ネイティブ実装)へ進む話が出てるよ🚀 (Microsoft for Developers)
- テストは Vitest が引き続き強くて、Vitest 4.0 で Browser Mode が安定化した流れもあるよ🧪✨ (Vitest)
次の第19章は、この「差し替えOKな設計」を 型とテストでガチガチに守る回になるよ🛡️✅ この章のコードをベースに、**“同じテストが全部の実装で通る”**をやるとめちゃ気持ちいいよ〜😆🎉