/note/tech

なぜ、SOLIDのリスコフ置換の原則(LSP)はもはや不要なのか?

要約:

■ 1. LSPの歴史的背景と意義

  • LiskovとWingの1994年論文で行動的型付けとして定式化された概念
  • 事前条件・事後条件・不変条件を含む振る舞いの保存を要求
  • マーチンは1996年にC++のパブリック継承を律する原則として説明
  • 当時の再利用は抽象基底クラスと継承が主役であったため有用な実務原則だった
  • LSPが有効だったのは継承中心の設計世界という前提が存在したためであり普遍原則ではない

■ 2. LSPが継承前提の原則であった理由

  • 長方形と正方形の問題の本質:
    • 数学の包含関係を可変オブジェクトに移植したことが誤りであり正方形が悪いわけではない
  • ソフトウェア設計は分類学ではなく契約の設計:
    • 設計で問うべきは「同じ仲間か」でなく「同じ契約で扱えるか」
    • 共通の操作がなければ親型は不要であり共通の操作があるならそこだけを抽象化すれば足りる
  • 古いOOP教育のAnimal-Dog-Catのような例が抽象化の誤解を招く:
    • 抽象化の入口が逆向きとなり役に立たない巨大な親クラスが生まれる

■ 3. 現代言語における継承の扱い

  • Java:
    • sealed classとsealed interfaceでpermitsにより継承できるクラスを明示制限
    • ライブラリのクライアントが任意の新種をぶら下げることを防ぐ設計方針
  • Kotlin:
    • 委譲パターンを実装継承の良い代替として明言しbyによる委譲を言語機能としてサポート
    • シールドクラスとシールドインタフェースで直接の下位型をコンパイル時に既知にし外部からの拡張を防ぐ
  • C#:
    • sealedで他クラスからの継承を禁止
    • デフォルトインタフェースメソッドで既存インタフェースを壊さずに拡張する仕組みを提供
  • 継承を持つ現代言語でもLSP問題が発生しやすい野放し継承を推奨していない

■ 4. LSPが吸収された先

  • ISP(インタフェース分離原則):
    • 利用者が必要とする操作だけを小さく切り出すことで長方形と正方形のような破綻が入口で消える
  • DIPと委譲:
    • 依存方向を抽象へ向け実装の再利用を継承でなく委譲で行うことで下位型が親の可変状態を持ち回らずに済む
    • 委譲中心設計では親の意味を壊しにくい構造になる
  • 型検査と閉じた階層:
    • TypeScriptの構造的型付けが名前でなく形で互換性を判定
    • JavaやKotlinのsealedが階層の増殖を閉じる
  • テスト:
    • 契約テストや回帰テストとして利用者側の期待を書き代替可能性を確かめる

■ 5. LSPをSOLIDに残し続けることの問題

  • 初学者がAnimal継承の例から入ることで設計の最初の問いが「親クラス名は何か」になる
  • 抽象化が契約の設計でなく分類の命名にすり替わる
  • 実際には半分以上の派生先でアンサポートになる巨大な基底クラスが生まれる
  • 「継承を正しく使えば世界をきれいに表現できる」という幻想が延命される
  • LSP違反以前の問題として継承構図そのものを最初に作ってしまうことが問題

■ 6. LSPが残る限定的な場所

  • 有効な文脈:
    • 継承ベースのAPIや古いフレームワーク
    • 基底クラス参照で差し替えを前提にしたライブラリ設計
    • JavaやC#の標準ライブラリ・既存の業務システム・GUIフレームワーク
  • 現在のLSPの位置づけ:
    • 「万人向けの基礎教養」でなく「ある設計条件で再登場する専門注意事項」
    • SRPやISPのように毎日意識する原則とは性格が異なる

■ 7. まとめ

  • LSPはもともと行動的型付けとして生まれ継承中心だった1990年代に有用な実務原則だった
  • 現代は継承あり言語でもシールドクラス・委譲・デフォルトインタフェースメソッドでLSP問題を言語機能側で吸収した
  • 置換可能性はISP・DIP+委譲・型検査と閉じた階層・テストへと分散吸収された
  • LSPは理論として尊重すべきだが現代の一般的な設計においてSOLIDの独立した柱として常設する必要はない
  • 継承中心時代の歴史的文脈とともに位置づけ直し必要な場面でだけ取り出して使うほうが筋が通っている