リスコフの置換原則
SOLID の L - サブタイプはスーパータイプと置換可能でなければならない原則
リスコフの置換原則とは
リスコフの置換原則 (Liskov Substitution Principle, LSP) は、SOLID 原則の L にあたる設計原則である。スーパータイプを期待して書かれたコードにサブタイプのオブジェクトを渡しても、そのコードの正しさが保たれるように継承関係を設計せよ、という要求だ。
出どころは Barbara Liskov が 1987 年の OOPSLA で行った基調講演「Data Abstraction and Hierarchy」(Addendum to the Proceedings of OOPSLA '87、17 から 34 ページ) である。そこで示されたのは規範ではなくサブタイプの定義で、型 S の各オブジェクト o1 について、T を用いて書かれたすべてのプログラム P の振る舞いが o2 を o1 に置き換えても変わらないような型 T のオブジェクト o2 が存在するなら、S は T のサブタイプである、という条件文の形をとっている。この考え方は後に Barbara Liskov と Jeannette M. Wing の論文「A Behavioral Notion of Subtyping」(ACM Transactions on Programming Languages and Systems 16 巻 6 号、1994 年 11 月、1811 から 1841 ページ) で、事前条件・事後条件・不変条件を用いた形に形式化された。
実務で押さえたいのは、これが文法の話ではなく振る舞いの話だという点である。継承を書いてコンパイラを通した時点で満たせている気になりやすいが、求められているのは呼び出し側から見た振る舞いが変わらないことなので、型検査を通っても壊れる設計は成り立つ。
違反の例
継承の題材として繰り返し引かれる長方形と正方形で見るのが早い。数学の上では正方形は長方形の特殊な場合なので、Square を Rectangle のサブクラスにするのは自然に見える。
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w: number) { this.width = w; this.height = w; } // 正方形は幅=高さ
setHeight(h: number) { this.width = h; this.height = h; }
}
// LSP 違反: Rectangle を期待するコードが壊れる
function resizeTo4x5(rect: Rectangle) {
rect.setWidth(4);
rect.setHeight(5);
return rect.area(); // Rectangle なら 20、Square だと 25 が返る
}
Square の実装そのものは間違っていない。壊れているのは、幅と高さを独立に設定できるという Rectangle が呼び出し側に与えていた前提であり、setWidth が高さまで書き換えることでそれが破られる。しかも型検査は通るので、コンパイラは何も教えてくれない。継承を疑うべきサインは数学的な分類ではなく、呼び出し側が当てにしている前提を守れるかどうかに現れる。
準拠の例
直し方は、継承で実装を分け合うのをやめて、共通の振る舞いだけをインターフェースとして切り出すことである。幅と高さを独立に変えられるという前提を Square に持ち込まなければ、置換しても壊れない。
// ✅ LSP 準拠: 共通のインターフェースで振る舞いを統一
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area() { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
area() { return this.side * this.side; }
}
// どちらも Shape として安全に使える
function printArea(shape: Shape) {
console.log(`Area: ${shape.area()}`);
}
LSP 違反のサイン
- サブクラスのメソッドが例外をスローする (親クラスではスローしない)
- サブクラスが親クラスのメソッドを空実装にする
instanceofで型チェックして分岐する必要がある- サブクラスが親クラスの事前条件を強化する
TypeScript での実践
クラスの継承だけの話ではなく、型どうしの置換可能性としても同じ構図が現れる。TypeScript の readonly number[] と number[] の関係がわかりやすい。
const scores: readonly number[] = [80, 90];
function addItem(arr: number[]) {
arr.push(42);
}
// error TS2345: Argument of type 'readonly number[]' is not assignable
// to parameter of type 'number[]'.
addItem(scores);
// 読み取りしかしないなら広い方の型で受ける (number[] も渡せる)
function sumItems(arr: readonly number[]): number {
return arr.reduce((a, b) => a + b, 0);
}
置換できる向きは一方通行である。number[] は readonly number[] が期待される場所へ渡せるが、逆はできない。書き換えられる配列を読み取り専用として扱っても呼び出し側の期待は裏切らないのに対し、読み取り専用の配列を書き換え可能なものとして扱うと push のような操作が成り立たなくなるからだ。この向きでいえば number[] が readonly number[] のサブタイプにあたる。継承関係があるわけではない点にも注意したい。標準の型定義では Array と ReadonlyArray はどちらも extends を持たない独立したインターフェースで、TypeScript は構造を見て代入できるかどうかを判定している。
見落としやすいのは、この検査が効くのはコンパイル時までだという点だ。readonly はトランスパイル後の JavaScript に痕跡を残さず、実行時の配列が凍結されるわけでもない。as で型を外したり any を経由したりすれば書き換えは通ってしまうので、実行時に本当に変更を防ぎたい場面では Object.freeze のような手段を別に用意する。
引数の型を決めるときの指針としては、読み取りしかしない関数は readonly を付けた広い型で受け、書き換えが必要な関数だけ書き換え可能な型を要求する。こうすると呼び出し側が渡せる値の幅が広がり、意図しない書き換えも型検査で止まる。
他の SOLID 原則との関係
LSP は開放閉鎖原則 (OCP) の前提条件だ。サブタイプが安全に置換できなければ、拡張に対して開いた設計は実現できない。
直し方の側はインターフェース分離の原則 (ISP) につながる。置換すると壊れる操作を型から外し、その操作を持つ型と持たない型に分けるという処方は、そのまま ISP の実践になる。上の長方形と正方形も、寸法を書き換える操作を共通の型から切り離し、面積を返す振る舞いだけを共有した結果として整合した。
LSP 違反の判定基準
LSP 違反になりやすい変更と、その直し方を以下に整理する。
| 変更 | 呼び出し側で起きること | 直し方 |
|---|---|---|
| サブクラスが例外を投げる | 親の型として呼んでいる箇所が想定外の例外で落ちる | その操作を持たない型として切り出す |
| 事前条件を強化 | 親では通っていた入力が、差し替えた途端に弾かれる | 受け付ける範囲は親と同じか広くする |
| 事後条件を弱化 | 親の保証を前提に書いた後続処理が崩れる | 返す値の保証は親と同じか強くする |
| 不変条件を破る | 親の型が守っていた前提が壊れ、離れた場所で矛盾が出る | 不変条件の検査を親と共通の場所に置く |
| メソッドをオーバーライドして何もしない | 呼んでも何も起きず、成功と区別できない | 操作を持つ型と持たない型に分ける |
リスコフの置換原則を扱う関連書籍も多い。
この記事は役に立ちましたか?
関連用語
SOLID 原則
オブジェクト指向設計の 5 つの基本原則で、保守性と拡張性の高いコードを実現する
開放閉鎖の原則
SOLID の O - ソフトウェアの構成要素は拡張に対して開かれ、修正に対して閉じているべきという設計原則
インターフェース分離の原則
SOLID の I - クライアントが使わないメソッドへの依存を強制しない原則
抽象クラス
直接インスタンス化できず、サブクラスに共通のインターフェースと部分的な実装を提供するクラス
オブジェクト指向
データと操作をオブジェクトとしてまとめ、現実の概念に近づけて設計する考え方
単一責任の原則
SOLID の S - クラスやモジュールが変更される理由は 1 つだけであるべきという設計原則
関連する記事
スマホ / パソコン入門書の選び方 - シニアの家族に贈る 1 冊の見つけ方
スマホとパソコンの入門書を「贈る側 / 教える側」の視点で選ぶガイド。機種と OS の一致、文字の大きさ、年度の確認という 3 条件と、Android / iPhone / Windows 11 それぞれの定番書、安全対策の 1 冊まで 2026 年 8 月時点の収載書から紹介します。
1 万行のコードより 1 冊の設計書が勝つ場面
大量のコードを書く力と、適切な設計を選ぶ力は別物です。コード量では解決できない問題に直面したとき、設計の知識がどう効くのかを具体例で解説します。
技術書の中古本 / 古本の賢い買い方 - 安く手に入れて賢く学ぶ
技術書を中古で買うときの判断基準と注意点を解説します。中古で買っていい本と新品で買うべき本の見分け方、状態チェックのポイントを紹介。