カテゴリ: 原則
種類: システム設計原則
起源: バーバラ・リスコフ、OOPSLA 基調講演 Data Abstraction and Hierarchy、1987年
別名: LSP、振る舞いサブタイピング、強い振る舞いサブタイピング、Liskov Substitution Principle
種類: システム設計原則
起源: バーバラ・リスコフ、OOPSLA 基調講演 Data Abstraction and Hierarchy、1987年
別名: LSP、振る舞いサブタイピング、強い振る舞いサブタイピング、Liskov Substitution Principle
先に答えると — リスコフの置換原則(Liskov Substitution Principle)は、サブタイプが親の振る舞い契約を守るときに限り、親の代わりに立てる、という設計規則です。利用者は、受け取ったものがどれかを知らなくて済みます。バーバラ・リスコフは1987年の OOPSLA 基調講演で置換の性質を述べ、ジャネット・ウィングと1994年に形式化しました。実務の検査は単純です。親のメソッドを呼ぶ前に具体的な種類を見なければならないなら、階層はすでに嘘をついています。
リスコフの置換原則とは?
リスコフの置換原則は、サブタイプが親タイプの期待される場所で使えなければならず、すでに利用者が頼っている正しさを壊してはならない、という設計規則です。型 S の各オブジェクト o1 に対し、型 T のオブジェクト o2 が存在し、T について書かれたすべてのプログラム P の振る舞いが、o1 を o2 の代わりに置いても変わらないなら、S は T のサブタイプである。これはコンパイラの検査ではありません。コンパイラが合わせるのは名前とシグネチャです。原則が問うのは約束です。親の利用者が仮定してよいことが、サブタイプを置いてもなお真であるか。 日常の絵は代講の教師です。時間割はなお「2時間目、数学」と書いてあります。その時間を回せる代講は置換できます。出席を取らない代講、本務が教室に座っていないと教えられない代講は、置換できません。椅子の人が変わったからといって、学校が一日を書き直す必要はないはずです。 コードでは、Square が Rectangle を継承する傷がよく出ます。Rectangle は、幅を変えても高さを変えなくてよいと約束します。辺を等しく保つ Square は両方を変えます。幅を設定して高さを読むコードは驚きます。家系図は「である」と書いた。契約は書いていません。オープン・クローズド原則 はこの規則を必要とします。古い利用者がどのサブタイプかを調べねばならないなら、新しいサブタイプは差し込めません。
リスコフの置換原則を3つの深さで理解する
- 入門: 役割を埋めると称するなら、その役割がすでに公示した仕事を果たす。単3のスロットに入るが電圧の違う電池は、形は同じでも契約は同じではありません。
- 実務: 継承する前に、親の約束を列挙する。利用者が渡してよいもの、返ってくると期待してよいもの、決して起きないこと。サブタイプはより寛大であってよい。より口うるさくなってはならない。
- 上級: 振る舞いサブタイピングはスナップショットであると同時に、履歴の制約です。可変オブジェクトは、親が排除した状態変化を持ち込めません。だから、途中を突けるスタックは、push と pop があってもスタックではありません。
起源
リスコフの置換原則は、チームの壁の標語としてではなく、型階層の問いとして始まりました。 当時 MIT Laboratory for Computer Science にいた バーバラ・リスコフ は、OOPSLA ‘87(オーランド、1987年10月4–8日)で基調講演 Data Abstraction and Hierarchy を行います。講演は会議の補遺(ACM、doi 10.1145/62138.62141)に収まり、のちに ACM SIGPLAN Notices 23(5): 17–34(1988年5月)にも載ります。彼女は1970年代、CLU 言語でデータ抽象を組み立てていました。OOPSLA では、Smalltalk 流の実装の継承は、利用者が信頼できる型階層とは別だ、と論じます。読んでいた論文は、スタックとキューを互いのサブタイプとして扱っていました。それは成立しません。後入れ先出し向けに書いたコードは、先入れ先出しを渡されると壊れます。 彼女と ジャネット・ウィング(Jeannette Wing)は、A Behavioral Notion of Subtyping(ACM Transactions on Programming Languages and Systems 16(6): 1811–1841、1994年11月、doi 10.1145/197320.197383)で、この考えに形式的な歯を与えます。サブタイプの要件は、型 T のオブジェクトについて証明できる性質は、サブタイプ S のオブジェクトについても成り立つ、というものです。ここでのサブタイピングは仕様の関係であり、クラスファイルの関係ではありません。 ロバート・C・マーティン は 1996年3月 の C++ Report の Engineering Notebook コラム “The Liskov Substitution Principle” で、この考えを日常のオブジェクト指向に運びます。実務の一行は次です。基底クラスへのポインタまたは参照を使う関数は、派生クラスのオブジェクトを、それが派生だと知らずに使えなければならない。マーティンの 2003年 の本 Agile Software Development: Principles, Patterns, and Practices(Prentice Hall)は、この検査を残します。2004年 前後、Michael Feathers はマーティンの「最初の五原則」を SOLID の頭字語に並べます。リスコフの置換原則はその「L」です。リスコフはデータ抽象、耐故障、分散計算の仕事で 2008年 の ACM チューリング賞を受けます。賞はこの一条より広い。名前が技術の口語に残った理由ではあります。
要点
リスコフの置換原則が本領を発揮するのは、新しい種類のものが古い差し込み口を埋めねばならないときです。追加機能の禁止ではありません。取り消してはならない約束についての規則です。1
「である」は契約であり、家系の相似ではない
フィールドをいくつか共有する、都合のよい親クラスを選ぶ、それだけではサブタイプになりません。親の利用者には、公示された振る舞いを受け取る権利があります。継承図を受け取る権利はありません。
fly() を継承してから例外を投げるペンギンは、「特殊な鳥」ではありません。親タイプが言い過ぎたのです。関心の分離 は仕事を分けるよう教えます。この原則は、偽の家系図でそれを貼り戻すな、と教えます。2
扉を狭くせず、結果も弱くしない
サブタイプは事前条件を弱めてよい(入力を増やす)し、事後条件を強めてよい(出力について多くを保証する)。矢印を逆にしてはなりません。親が「任意の正の金額」と言ったのに、子が100未満を拒むなら置換できません。親が「空でないリストを返す」と言ったのに、子が空を返すなら置換できません。余分なメソッドは構いません。古いメソッドをより静かで口うるさい版にするのはいけません。
3
不変条件と履歴は、入れ替えたあとも残る
親がすべての正当なオブジェクトについて真だと言ったことは、置換後も真でなければなりません。文字列だけを持つはずのプロパティ一覧が、親のメソッド経由で整数を詰め込まれるなら、不変条件はすでに壊れています。履歴も同様です。親が値を縮めさせなかったなら、
setWidth が高さも切る子は、利用者が閉じたと思っていた過去を書き換えています。4
利用者が具体的な種類を見ねばならないなら、階層はすでに失敗している
実行時型に対する
if の列や、「この特別なサブクラスでは呼ぶな」というコメントは匂いです。新しいいとこが来るたびに、それらの利用者は変わります。この原則と オープン・クローズド原則 は一緒に壊れます。下にはしばしば 単一責任の原則 があります。一つの型が二つの仕事を担い、継承が糊になっているのです。応用場面
一つの差し込み口を複数の種類が埋め、古い利用者が警告ラベルを読まずに動き続けてほしいときに、リスコフの置換原則を使います。似た名詞をすべて一つの親に載せてはいけません。次のいとこを足す前に、嘘の階層を割る
辺を等しく保つ Square は、自由に寸法を変えられる Rectangle ではありません。引き出せない定期預金は、親が
withdraw を約束した口座ではありません。より細い親を作る——あるいは親を作らない——そして、実際に守れるメソッドだけを渡します。採点スクリプトの課題種別も同じ切り口です。「点数を直せない小テスト」は、残りのスクリプトが編集可能だと仮定する「採点対象」ではありません。代理の役割は、バッジではなく仕事で見る
発注書に署名できない代理マネージャは、手続きがすでに頼っているマネージャの置換ではありません。同じ署名権を副に渡すか、署名をその差し込み口の仕事から外す。職名は継承図です。ワークフローが契約です。ここでの驚きは、置換の失敗であると同時に 最小驚きの原則 の失敗でもあります。
代役が本当に務められる部の役職を書く
学校や地域の集まりは「会計」を置き、控えが銀行に入れず、報告を出せず、鍵も持てないことに気づきがちです。会則が約束したのは役割です。控えが埋めたのは名前です。金の報告、鍵の保管、会議への出席——役職が果たすべき仕事を先に書き、それから誰が立てるかを問う。本人が部屋にいないと回らない当番表は、当番表ではありません。
「互換」の公的用紙を、見た目ではなく契約として扱う
窓口が通常どおり処理できない代替身分、みなし税表、仮の許可は置換できません。市民は余分な来庁でそのずれを払います。公開された用紙が「合う添付ならどれでもよい」と書くなら、合う添付は実際にどれでも通らねばなりません。通らないものがあるなら、制限を親の用紙に刷る。同じ題名のままのサブタイプに隠さない。
事例
リスコフの置換原則を運用上の傷として見る、数字のある窓は Java のProperties クラスです。ライブラリの一クラスが型理論の全部だ、という主張ではありません。
JDK 1.0(1996年)以来、java.util.Properties はストリームから読み、ストリームへ書ける、文字列対文字列の永続一覧です。実装は Hashtable のサブクラスでした。Hashtable は null でない任意のキーと値を受け付けます。親の公開契約は、子の公開された仕事より広い。その継承のゆえに、put と putAll は Properties オブジェクトに対して呼べます。公式の Java API は今も、その使用を「強く非推奨」と書きます。文字列でないキーや値を呼び出し側が入れられるからです。その「損なわれた」オブジェクトに store や list を呼ぶと、呼び出しは失敗します。典型はクラスキャストです。子は親のメソッドを取り下げていません。守れない扉を継承したのです。
人々は何十年もこれを指差してきました。2016年5月17日、OpenJDK の不具合 JDK-8157123 が “java.util.Properties class is violating the Liskov’s Substitution Principle (LSP)” という題で起票されます。提案された修正はコンポジションでした。Properties の中に表を持ち、Hashtable であることをやめる。不具合は Won’t Fix で閉じます。公開された型関係は既存の利用者への約束です。extends を外すことは互換性破壊になります。JDK 9(2017年)では、クラスが作り直され、エントリは継承した表には住みません。リリースノート JDK-8175789 は、値が内部の並行マップにあり、デッドロックを減らすため getter の同期が外された、と記録します。現行 OpenJDK のソースのコメントはなお、Properties は継承した Hashtable に値を格納しない、と書きます。クラスは今も Hashtable を継承します。2026年時点でも、階層の嘘は公開 API に残っています。
事例が見せるのは、好みではなくロックインとしての原則です。「これはあれである」と公開したあと、中身は変えられ、警告は書けます。置換を静かに撤回することはできません。境界の注記: setProperty を使い、文字列を保てば、Properties は動きます。失敗しているのは型の主張であり、すべての呼び出し箇所ではありません。新しい設計への教訓は、2016年に採用できなかった回り道です。表を持たせる。継承しない。
限界と失敗パターン
親の契約がすでに雑袋であるとき、リスコフの置換原則は失敗します。Java のList の「任意操作」——定サイズや変更不可のリストで add が例外を投げてよい——は、ライブラリ全体の妥協です。実装者はみな部分的なサブタイプになります。利用者は脚注を読まねばなりません。それは脚注を増やす許可ではありません。親が広すぎる、という警告です。また例外を投げるいとこを積む前に、親を細くするか、インターフェイスを割ってください。
公開された嘘の事後修復としても失敗します。Properties がその展示です。正しい設計はコンポジションであり、正しい設計はもう取れません。20年物の階層を「学びの機会」と呼んでも、それを写してよいことにはなりません。1996年の extends ではなく、2016年の回り道を写してください。
よくある誤用は、メソッドの半分が NotSupported の神クラスです。飛べない鳥、引き出せない口座、電源を切れない装置は、「特殊化したサブタイプ」ではありません。大きすぎる親の、口うるさい子です。もう一つの誤用は、振る舞いが分かれたあとに、コード再利用のために継承することです。その再利用は共有ヘルパーに置きます。偽の「である」のない DRY です。親が安全だと約束した呼び出しでサブタイプが例外を投げるのも、フェイルファスト と衝突します。失敗はします。ただし、親が安全だと言った他人の呼び出しで、遅く失敗します。
よくある誤解
英語名は、「コンパイルできれば置換できる」「未サポートを投げればよい」、SOLID をクラスの性格診断にする、といった標語と衝突します。サブクラスが親に対してコンパイルできれば、リスコフの置換原則は満たされる
サブクラスが親に対してコンパイルできれば、リスコフの置換原則は満たされる
コンパイラが調べるのは名前、型、可視性です。
setWidth が利用者が思っていた意味のままか、プロパティ一覧がなお文字列だけかは調べません。振る舞いサブタイピングは、すでに動いているプログラムについての主張です。緑のビルドは必要であり、十分ではまったくありません。気に入らないメソッドでは、サブタイプは未サポートを投げてよい
気に入らないメソッドでは、サブタイプは未サポートを投げてよい
親が日常利用の一部だと公示したメソッドで例外を投げるのは、約束の撤回です。標準ライブラリの任意操作は知られた汚れであり、写す型ではありません。そのメソッドは親に置くべきではないか、すべてのサブタイプが守るべきです。「ここでは呼ぶな」というコメントは契約の穴であり、設計ではありません。
リスコフの置換原則はオブジェクト指向のクラスだけの話だ
リスコフの置換原則はオブジェクト指向のクラスだけの話だ
リスコフの1987年の講演がオブジェクトを使ったのは、それがその会議だったからです。同じ形は役割、API、文書、代理にも出ます。差し込み口が「どの会計でも」「どの添付でも」「どの支払い方法でも」と言うなら、代わりのものはその仕事を果たさねばなりません。旧い読み手が壊れる「互換」JSON フィールドは、class キーワードのない同じ失敗です。
関連概念
これらのページは、新しい種類のものを古い差し込み口に入れ、すでにその差し込み口に頼っている人を驚かさない、という同じ問題の隣に座っています。オープン・クローズド原則
新しい振る舞いは新しい部品として来る。この原則は、その部品が古い差し込み口に本当に座れるかの検査です。
単一責任の原則
モジュールごとに変わる理由は一つ。偽のサブタイプは、一つの親が二つの仕事を担うときに現れます。
関心の分離
異なる決定を離しておく。細い親は、深い木ではなく、きれいな切り口であることが多い。
最小驚きの原則
コンパイルできて振る舞いが奇妙な置換は、作りつけの驚きです。
フェイルファストの原則
親が安全だった場所で例外を投げるサブタイプは、他人の呼び出しで遅く失敗します。
デメテルの法則
すぐ隣とだけ話す。いとこの内部を覗く利用者は、すでに親の契約を迂回しています。