Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Liskov Substitution Principle

2025年10月05日に「Dev.to」が公開したITニュース「Liskov Substitution Principle」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

リスコフの置換原則は、親クラスのメソッドが子クラスで不要な場合に設計不整合を避ける原則だ。例えば、エンジンを持つ親クラスを自転車が継承すると矛盾する。これを解決するには、エンジン有無で継承元クラスを適切に分け、システムの整合性と拡張性を高める。

出典: Liskov Substitution Principle | Dev.to公開日:

ITニュース解説

リスコフの置換原則(LSP: Liskov Substitution Principle)は、オブジェクト指向プログラミングにおける重要な設計原則の一つであり、プログラムの堅牢性や拡張性を高めるために不可欠な考え方だ。この原則は簡単に言えば、「親クラスが使える場所であれば、その子クラスも問題なく使えるべきである」と提唱する。つまり、子クラスは親クラスの振る舞いを壊してはならないということだ。

この原則が守られないと、どのような問題が起こるのか。例えば、ある親クラスが持つメソッドが、特定の子クラスでは意味をなさない場合、その子クラスでそのメソッドを無理やり実装すると、予期せぬ動作を引き起こしたり、プログラムの意図が不明確になったりする。結果として、設計が破綻しやすくなり、将来の機能追加や変更が困難になるだけでなく、バグの原因にもなりかねない。

具体的な例を見てみよう。もしVehicleという親クラスがあり、その中にhasEngine()というメソッドがあると仮定する。このメソッドは、その乗り物がエンジンを持っているかどうかを真偽値で返すものだ。CarMotorcycleといったクラスがVehicleを継承する場合、これらは実際にエンジンを持っているため、hasEngine()メソッドを「エンジンを持っている」と適切にオーバーライドできる。これは問題ない。

しかし、もしBicycle(自転車)クラスもVehicleを継承するとどうなるだろうか。自転車はエンジンを持たない。この場合、BicycleクラスでhasEngine()メソッドをオーバーライドする際に、「エンジンを持っていない」という意味でnullを返したり、「false」を返したりする選択肢が考えられる。ここでnullを返す実装をしてしまうと、リスコフの置換原則に違反してしまう。

なぜnullを返すことが問題なのだろうか。プログラムの他の部分で、Vehicle型のオブジェクトを受け取り、そのhasEngine()メソッドの結果に基いて何らかの処理を行うコードがあるとする。例えば、「もしエンジンがあるならば、燃料を補給する」といったロジックだ。このコードは、VehicleオブジェクトがhasEngine()を呼び出した際に、意味のある真偽値(truefalse)が返されることを期待している。もしそこにBicycleオブジェクトが渡され、hasEngine()nullを返した場合、そのコードは予期せぬNullPointerException(ヌルポインタ例外)などのエラーを引き起こす可能性がある。これは、親クラス(Vehicle)を期待して書かれたコードが、子クラス(Bicycle)によってその期待を裏切られる状態であり、リスコフの置換原則が破られている典型的な例となる。プログラムは、Vehicleオブジェクトに対して一貫した振る舞いを期待するが、Bicycleはその期待を満たさないのだ。このような設計は、一見すると動作するように見えても、将来的に大きな問題を引き起こす「技術的負債」となりうる。

このようなリスコフの置換原則違反を解決し、より良い設計にするための方法がリファクタリングだ。問題の本質は、「エンジンを持つ」という特性が、全てのVehicleに共通するわけではない点にある。そこで、VehicleクラスからはhasEngine()メソッドを取り除き、より汎用的な「乗り物」としての特性のみを残す。

次に、エンジンを持つ乗り物とそうでない乗り物を明確に区別するために、新しい中間クラスを導入する。例えば、EngineVehicleというクラスをVehicleから派生させる。このEngineVehicleクラスに、初めてhasEngine()メソッドを定義するのだ。こうすることで、エンジンを持つことを前提とするメソッドは、EngineVehicleとその子クラスのみに存在することになる。

この変更後、CarMotorcycleEngineVehicleを継承するように変更する。これにより、これらのクラスは自然にhasEngine()メソッドを持つことになり、その振る舞いも「エンジンを持つ」と正しく実装される。一方、BicycleVehicleを直接継承する。VehicleにはhasEngine()メソッドがないため、Bicycleはエンジンについて「嘘をつく」必要がなくなるのだ。Bicycleが持つべきメソッド(例:numberOfWheels()など)だけを実装すればよくなる。

このリファクタリングによって得られるメリットは大きい。まず、コードの整合性が保たれる。クライアントコードは、EngineVehicle型のオブジェクトに対してはエンジン関連の操作を安全に期待でき、Vehicle型のオブジェクトに対してはエンジン関連の操作を期待しない。これにより、どのオブジェクトに対しても、その型が持つべき振る舞いのみを期待できるようになり、予測不能なバグのリスクが大幅に減少する。

さらに、将来的な拡張性も向上する。例えば、電動アシスト自転車のようにエンジンを持たないがモーターを持つ乗り物を追加する場合でも、現在の設計は柔軟に対応できる。新たにElectricVehicleのようなクラスをVehicleから派生させ、そこにモーター関連のメソッドを定義すればよい。これにより、それぞれの乗り物の種類に応じた適切な継承関係とメソッドの定義が可能となり、不必要なメソッドを強制的に実装するといった無理が生じなくなる。

このように、リスコフの置換原則を意識したクラス設計は、単にコードを動かすだけでなく、プログラムの全体的な品質、つまり保守性、拡張性、そして堅牢性を高める上で非常に重要だ。初心者にとっては難しく感じられるかもしれないが、オブジェクト指向プログラミングで長期的に安定したシステムを構築するためには、避けては通れない、そして学ぶ価値のある重要な原則なのである。常に「子クラスは親クラスの振る舞いを壊していないか」という視点を持って設計に取り組むことが、良いソフトウェアエンジニアになるための第一歩となる。

関連コンテンツ