保守性と拡張性を高めるためのSOLID原則とシステム設計
第5話: リスコフの置換原則(1)概要
(最終更新日:2024.09.24)
(絵が小さい場合はスマホを横に)
子クラスは親クラスを代替できる!
リスコフの置換原則(LSP)は、サブ(子)クラスが基底(親)クラスの代替として適切に機能することを求める原則だ。
子クラスは親クラスの契約を守り、振る舞いを変更せずに拡張する必要がある。
正しい使い方では、共通の抽象クラスやインターフェースに基づいて子クラスを実装し、一貫した動作が保証される。
逆に、子クラスが親クラスの期待を壊す設計はLSP違反となり、システム全体の保守性が低下する。
本原則に従ったコーディングを今回は学ぼう。
[目次]
1.リスコフの置換原則とは
リスコフの置換原則(Liskov Substitution Principle, LSP)**は、オブジェクト指向設計のSOLID原則の1つであり、
特にクラスの継承に関する重要な指針を示す。バーバラ・リスコフが提唱したこの原則は、
子クラスは、その親クラスで期待される動作を完全に満たす必要があるという考え方に基づく。
つまり、子クラスは親クラスの代わりとして常に適切に動作しなければならないという原則だ。
1.1 リスコフの置換原則とは
リスコフの置換原則(LSP)の基本的な定義は次の通りだ。
「子クラスは、その親クラスと置換可能でなければならない。」
これを具体的に言うと、ある基底クラスのインスタンスを子クラスのインスタンスに置き換えても、
プログラムの正しい動作が保証されなければならないということになる。
子クラスが親クラスの基本的な契約(つまり、振る舞いや仕様)を守らなければならないという意味になる。
親クラスの契約(振る舞い、仕様)を守る
1.2 2. LSPの目的
LSPの目的は、継承を使ったクラス設計において、継承関係を持つクラスが期待通りに動作し、システム全体が安定して動くようにすることだ。
LSPが守られていないと、子クラスに置き換えた時に不正な挙動や予期せぬエラーが発生するリスクがある。
1.3 リスコフの置換原則の例
良い例:LSPに従った設計
Shapeというクラスを例にしてみる。
ShapeはgetArea()という面積を計算するメソッドを持っている。
これを円や四角形などの具体的な図形クラスで継承する場合、それぞれの子クラスが親クラスの振る舞いをきちんと守る必要がある。
下記の設計では、RectangleもCircleもShapeのサブクラスとして親クラスのgetArea()メソッドを正しく実装している。
つまり、どちらの子クラスもShapeの代わりに使われても期待通りに動作し、LSPに従う。
良い例
悪い例:LSPに違反した設計
次に、リスコフの置換原則に違反する例を示す。
Rectangleを継承したSquareクラスがあるが、Squareは幅と高さが常に等しいため、Rectangleの振る舞いと異なる挙動を示す可能性がある。
下記の例では、SquareがRectangleを継承するが、SquareはRectangleとして期待される挙動を守っていない。
Rectangleでは幅と高さが異なる場合があるが、Squareでは常に同じ値でなければならない。
この違いにより、SquareをRectangleとして使用すると、プログラムが予期しない動作をする可能性があり、LSPに違反している。
悪い例
1.4 LSPを守るための設計方法
リスコフの置換原則を守るためには、以下の点に注意して設計を行う必要がある。
子クラスは親クラスの契約を破らない:子クラスは、親クラスで定義されている振る舞いやメソッドの意味を変えてはいけない。
親クラスに置き換えたときに同じように動作するように設計する。
事前条件や事後条件の強化、弱化は避ける:子クラスで親クラスの事前条件(メソッドを呼び出す前の条件)を強化してしまうと、
期待した通りに動作しなくなる場合がある。また、事後条件(メソッドを呼び出した後の結果)を弱化することもLSP違反になる。
親クラスで不要なメソッドを定義しない: もし子クラスが特定の親クラスのメソッドを正しく実装できないのであれば、
そのメソッドは親クラスに定義すべきではない。例えば、飛べない鳥が親クラスでfly()メソッドを持つのは不適切になる。
1.5 まとめ
リスコフの置換原則は、サブクラスが親クラスの代わりに問題なく動作するように設計するための重要な原則だ。
これにより、システム全体の安定性と拡張性が向上する。
LSPに違反しない設計を行うことで、クラスの継承階層が正しく機能し、サブクラスを導入する際に予期せぬバグが発生するリスクを減らせる。
2.子クラスと継承の正しい使い方
ここでは、リスコフの置換原則(Liskov Substitution Principle, LSP)に従い、子クラスと継承をどのように正しく使うべきかを説明する。 LSPは、子クラスが親クラスの代わりとして常に適切に機能することを保証するための原則で、 これにより、継承階層が壊れることなく、拡張性や保守性を高めることができる。
2.1 子クラスは親クラスの代替として常に機能する
LSPの基本は、子クラスが親クラスとして問題なく機能するという点にある。
子クラスは、親クラスのインスタンスと置き換えても、システムが正しく動作するように設計されていなければならない。
これが実現されていれば、コードの保守や拡張が容易になり、予期せぬエラーやバグのリスクが軽減される。
悪い例:親クラスBirdにfly()というメソッドが定義されている場合、
飛べない鳥(例えばペンギン)をこのクラスのサブクラスとして継承するのは問題だ。
ペンギンは飛べないため、fly()メソッドを正しく実装できない。
この場合、PenguinクラスをBirdクラスの代わりに使用すると、例外が発生し、システムが予期しない動作をする。LSPに違反する。
悪い例
良い例:この問題を解決するためには、共通の動作を表す抽象メソッドを定義し、 子クラスごとに適切な実装を行う必要がある。例えば、親クラスでmove()という抽象的な動作を定義すると、飛ぶ鳥と泳ぐ鳥を分けられる。 この設計では、どちらのクラスもBirdとして置き換えることができ、move()メソッドを適切に実装している。 LSPが守られ、親クラスの代替として問題なく機能している。
良い例
2.2 メソッドの動作や契約を破らない
親クラスで定義されているメソッドの契約(振る舞いや動作の約束)を子クラスで破らないことが重要だ。
親クラスで定義されている振る舞いが、子クラスでもそのまま守られる必要がある。
例えば、親クラスが返す値やエラーの処理などの契約を、子クラスで変更すると、置き換えた際に意図しない結果が発生する可能性がある。
親クラスと子クラスの間で同じメソッド名が使われていても、サブクラスが親クラスの動作を変更してしまうと、LSP違反となる。
拡張すれども、変更せずという原則が重要になる。
2.3 子クラスが拡張されるべきであって、制約されるべきではない
継承を使う際には、サブクラスは親クラスの機能を拡張するために使うべきだ。
親クラスの機能を弱めたり、使用できなくすることは避ける。
例えば、以下のように継承改装を適切に設計する。1.3で紹介した悪い例のように、RectangleクラスからSquareクラスを継承するのではなく、
ShapeクラスからSquareクラスとRectangleクラスを継承して、具体的な実装をすればよい。
良い例
2.4 LSPを守るためのチェックリスト
リスコフの置換原則を守るために、以下のポイントをチェックすることが重要だ。
メソッドの引数や戻り値の型が一致しているか:子クラスが親クラスのメソッドをオーバーライドする場合、
メソッドの引数や戻り値は親クラスと一致していなければならない。
事前条件・事後条件を変更していないか:子クラスがメソッドを実装する際、親クラスで要求される条件を弱めたり、
返される結果を変えたりしてはいけない。
例外の処理が親クラスと整合しているか: 親クラスのメソッドが特定の例外をスローする場合、
サブクラスはその例外処理を変更するべきではない。
振る舞いが一貫しているか: 子クラスが親クラスの代わりとして適切に動作するかを確認するため、
ユニットテストを行い、振る舞いの一貫性を保証する。
2.5 まとめ
子クラスと継承を正しく使うためには、リスコフの置換原則を守り、子クラスが常に親クラスの代わりとして機能できるように設計することが重要だ。
親クラスの契約を破らず、サブクラスがその拡張として適切に振る舞うことを確認することで、システムの柔軟性と保守性を高めることができる。
3.まとめ
今回はリスコフの置換原則(LSP)の第1回として、リスコフの置換原則の概念と子クラスと継承の正しい使い方について学んだ。 親クラスと子クラスの間で「抽象的な契約」が守られていれば、後からその関係を追いかける必要がなく、 どのクラスでも同じように安心して機能を利用することができる。ぜひ、今回学んだ作法を身に着けよう。