LSP: 開発におけるバーバラ・リスコフの置換原則の本質

著者: IT Sectr 公開日: 2026-05-12 読了時間: 9 分

LSP(Liskov Substitution Principle)はSOLIDの第3原則であり、オブジェクト指向プログラミングにおける正しい継承の条件を定義します。この原則は1987年にバーバラ・リスコフによって定式化され、次のように形式化されました:SがTのサブタイプである場合、T型のオブジェクトはプログラムの特性を変更することなくS型のオブジェクトに置き換えることができます。ロバート・マーチンの著書 Clean Architecture(2017)で述べられているように、置換原則はサブクラスが基底クラスの契約を弱めてはならないことを要求します。

重要なポイント

  • LSP — リスコフの置換原則、正しい継承に関するSOLIDの第3原則
  • サブクラスは基底クラスの契約を維持しなければならない — 事前条件と事後条件
  • LSP違反は「正方形と長方形の問題」やスローされた例外に現れる
  • コンポジションはLSP準拠のために継承より好ましいことが多い
  • 契約による設計(Design by Contract)— LSPを検証する正式な方法

LSP(Liskov Substitution Principle)とは?

LSP(Liskov Substitution Principle)は、1987年のOOPSLAカンファレンスでバーバラ・リスコフによって定式化された置換原則です。形式的な定義:q(x)をT型のオブジェクトxの証明可能な特性とします。このとき、q(y)はS型のオブジェクトyについて証明可能でなければなりません。ここでSはTのサブタイプです。簡単に言えば:サブクラスのオブジェクトは、基底クラスで動作するコードがサブクラスでも正しく動作し続けるように振る舞わなければなりません。

実践的には、LSPはサブクラスが基底クラスの契約に違反してはならないことを意味します。契約には事前条件(メソッドを呼び出すために必要なもの)、事後条件(呼び出し後に保証されるもの)、および不変条件(オブジェクトの存続期間中持続する条件)が含まれます。サブクラスが事前条件を強化したり事後条件を弱めたりすると、それがLSP違反です。

LSP違反の古典的な例は、長方形から継承する正方形です。長方形のsetWidthメソッドは幅を設定しますが、正方形では幅と高さの両方を設定します。長方形の動作(一方の辺を変更しても他方に影響しない)を期待するクライアントは、予期しない結果を得ます。正方形は長方形の有効なサブタイプではありません

LSPの形式的条件

LSPは正しい継承のために3つの条件を定めています:サブクラスの事前条件は基底クラスの事前条件より強くてはならず(サブクラスはより多くを要求しない)、サブクラスの事後条件は基底クラスの事後条件より弱くてはならず(サブクラスはより少なく保証しない)、基底クラスの不変条件はサブクラスで維持されなければなりません。これらの条件はバートランド・メイヤーによる契約による設計ルールとして知られています。

少なくとも1つの条件が違反されると、ポリモーフィズムを使用するコードが失敗する可能性があります。コンパイラは意味的な契約ではなく、構文上の契約のみをチェックします。したがって、LSPは静的型付けの問題ではなく、アーキテクチャの規律の問題です。

リスコフの置換原則の仕組み

LSPのメカニズムは型の動作互換性に基づいています。クラスSがクラスTから継承する場合、クライアントコードはTが期待される場所でSを動作を変更することなく使用できなければなりません。これにはメソッドシグネチャだけでなく、そのセマンティクスも含まれます。

LSPはサブクラスが新しい動作を追加することを禁止していません。基底クラス用に書かれたコードの期待に違反することは禁止されています。基底クラスがsaveメソッドが例外をスローしないことを保証する場合、サブクラスは例外をスローしてはいけません。基底クラスが非負の値を返す場合、サブクラスは負の値を返してはいけません。

実際のプロジェクトでは、LSPはサブクラスのメソッドに条件付きロジックを追加するときによく違反されます:「条件なら例外をスロー」、「条件ならnullを返す」。これらの「驚き」はポリモーフィズムを損ない、クライアントコードに呼び出し前にオブジェクトの型をチェックさせることになります — これはオブジェクト指向設計の考え方そのものに反します。

モバイルプロジェクトでは、基底ViewModelを作成する際に典型的なLSP違反が発生します。BaseViewModelがonClearedメソッドですべてのリソースを解放することを保証し、サブクラスがこのメソッドを空でオーバーライドすると、ポリモーフィックなonCleared呼び出しによるリソース解放に依存するコードは正しく動作しません。LSPはサブクラスがsuper.onCleared()を呼び出すか、自身で同じ作業を実行することを要求します。LifecycleObserverを介したコンポジションは、ライフサイクル管理におけるLSP違反を排除する代替手段です。

コード内のLSP違反の兆候

LSP違反の主な指標には以下が含まれます:メソッドを呼び出す前にinstanceofやisでオブジェクトの型をチェックする、空のメソッド実装(スタブ)、NotImplementedErrorやUnsupportedOperationExceptionをスローする、値の代わりにnullを返す。これらの各パターンは、サブクラスが有効なサブタイプではないことを示しています。

もう一つの一般的な兆候は、「〜である」(is-a)関係をモデル化するためではなく、コード再利用を目的とした継承です。Birdクラスにはfly()メソッドがあります。PenguinクラスはBirdから継承し、fly()を空または例外をスローするようにオーバーライドします。これはLSP違反です:ペンギンは鳥の有効なサブタイプではありません

モバイル開発では、スタブメソッドを持つ基底ViewHolder、Fragment、またはViewControllerクラスを作成する際にLSPが違反されます。サブクラスが基底クラスのメソッドの半分を使用しない場合、継承が誤って選択されています。コンポジションまたはインターフェース分離が問題をより正しく解決します。

LSPテスト

LSPをチェックする簡単なテスト:基底クラスの契約(戻り値、例外、副作用)を検証する単体テストを書きます。このテストをすべてのサブクラスで実行します。テストが失敗した場合、LSPが違反されています。このアプローチは「基底クラスの契約を通したテスト」と呼ばれます。

Androidプロジェクトでは、このようなテストはViewModelとRepositoryに役立ちます。BaseViewModelがエラーの前にLoading状態を保証し、サブクラスがLoadingなしでエラーをスローする場合、テストはCI段階でLSP違反を検出します。

モバイル開発におけるLSPの例

ClickListener処理を使用したAndroidの例を見てみましょう。基底実装が何かを保証し、サブクラスがそれに違反するときにLSP違反が発生します。

kotlin
// 保証付きの基底クラス:onClickが呼び出されます
open class BaseClickListener {
    open fun onClick(view: View) {
        // 基本的な処理
    }
}

// LSP違反:サブクラスが例外をスローする条件を追加
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// 正しい解決策:契約は違反されていません
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

DataSourceプロトコルを使用したiOSの例は、データの代わりにnilを返すことでLSP違反を示しています:

swift
// 契約付きのプロトコル:データまたはエラーを返す
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// LSP違反:エラーなしでnilを返す
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // エラーの代わりに空の配列
    }
}

// 正しいLSP準拠
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

実用的なルール:サブクラスが基底クラスの契約を果たせない場合、サブクラスであるべきではありません。代替案は、最小限の契約でインターフェースを抽出し、各型で独自の方法で実装することです。

LSPと継承:コンポジションを選ぶべき時

コンポジションは「〜である」(is-a)関係があいまいまたは条件付きである状況で継承より好ましいです。古典的な例:ManagerはEmployeeですか?はい。しかし、Squareは有効なRectangleですか?LSPは「いいえ」と言います。継承の正しさに疑問がある場合、コンポジションを選びましょう。

モバイル開発では、コンポジションは依存性注入を通じてよく使用されます:基底クラスから動作を継承する代わりに、クラスはコンストラクタを通じてそれを受け取ります。ViewModelはRepositoryから継承せず、依存関係として受け入れます。これにより、定義上LSP違反が排除されます — 継承がなければ、契約違反もありません。

継承をコンポジションに置き換えるべき兆候:サブクラスが基底クラスの一部のメソッドを使用しない、サブクラスがメソッドを空のスタブでオーバーライドする、クライアントコードがinstanceofでオブジェクトの型をチェックする。これらの場合、継承が誤って選択され、LSPが違反されています。

インターフェースによる解決策

インターフェースは継承なしでLSPの問題を解決します:各型は必要なメソッドのみを実装します。fly()メソッドを持つ共通の基底Birdクラス(Penguinは飛べない)の代わりに、飛ぶ鳥だけが実装するFlyableインターフェースを使用します。Penguinはfly()メソッドなしでBirdを実装します — LSPは違反されません。

Androidアーキテクチャでは、このアプローチは分離されたUseCaseインターフェースを通じて適用されます:getAll、getById、save、deleteメソッドを持つ1つの大きなUseCaseの代わりに、別々のGetItemsUseCase、SaveItemUseCaseインターフェースを使用します。クライアントは必要なインターフェースにのみ依存し、そのインターフェースを実装するクラスはLSPの観点から正しいです。

よくある質問

LSPは単純な継承とどう違うのですか?

継承は言語のメカニズムであり、LSPはそのメカニズムを正しく使用するためのルールです。継承はシグネチャの互換性(構文)を保証し、LSPは動作の互換性(セマンティクス)を要求します。LSPなしの継承は、実行時に壊れるポリモーフィズムをもたらします。

サブクラスでのnullは常にLSPに違反しますか?

基底クラスが非nullの戻り値を保証している場合 — はい。契約がnull(オプション値)を許可している場合 — いいえ。LSPはnullを禁止しているのではなく、契約を弱めることを禁止しています。基底クラスのドキュメントを調べ、サブクラスの契約が互換性があるか確認してください。

SwiftのプロトコルにはLSPはどのように適用されますか?

LSPはプロトコルにもクラスと同様に適用されます。プロトコルの実装は意味的な契約に従わなければなりません:プロトコルがメソッドをnon-throwingとして定義する場合、実装はエラーをスローしてはいけません。Swiftはコンパイラレベルでこれをチェックしません — 責任は開発者にあります。

sealed classを使用するとLSP違反は発生しますか?

KotlinのSealed classは、階層が閉じておりコンパイラに認識されているため、特別なケースです。LSPはsealed classにはあまり適用されません。なぜなら、すべてのサブタイプがwhen式で明示的に列挙されているからです。sealedサブクラスのエラーはローカルであり、隠れたポリモーフィックエラーではありません。

プロジェクトでLSP準拠をテストするには?

基底クラスに対して、すべてのサブクラスで実行されるパラメータ化テストを書きます。テストは主要な動作契約(戻り値、例外、状態)を検証します。いずれかのサブクラスでテストが失敗した場合、LSPが違反されています。CIでは、このようなテストがポリモーフィックコードの回帰を防ぎます。

まとめ

  • LSP(Liskov Substitution Principle)— 置換原則、SOLIDの第3、継承の意味的互換性について
  • サブクラスは基底クラスの契約を維持しなければならない:事前条件、事後条件、不変条件
  • instanceofチェックと空のメソッドオーバーライドはLSP違反の主な兆候
  • コンポジションとインターフェースは、継承が不適切な場合にLSPの問題を解決する
  • 正方形と長方形の問題はサブタイプの非互換性の古典的な例
  • すべてのサブクラスに対して実行される基底クラスの契約テストは、CIでLSP違反を検出する
  • KotlinのSealed classは、コンパイラに認識された閉じた階層によりLSPリスクを低減する

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください