「リスコフの置換原則」における簡単な理解
「リスコフの置換原則」とは簡単に言うと?
「リスコフの置換原則」とは、簡単に言うと、「is a」関係(継承、実装)が成り立つ条件を説明したものです。一般に、以下の条件を満たせば成り立っていると確認でき、そうでなければ継承や実装を行ってはならないとされています。
派生クラス(サブクラス)は、
- コンストラクタまたはオーバーライドしたメソッドの引数の条件(事前条件)が、
基底クラス(スーパークラス)と同等またはより弱くてはならない(反変性)。 - コンストラクタまたはオーバーライドしたメソッドの返り値の条件(事後条件)が、
基底クラス(スーパークラス)と同等またはより強くてはならない(共変性)。 - 基底クラス(スーパークラス)のメソッド内において定められているルール(不変条件)を、
守らなければいけない
「なぜそうなのか」 具体的な説明
事前条件
仮に、上記の条件とは反対に「事前条件が、基底クラス(スーパークラス)より強い」場合を考えます。
例えば、「Fish(魚)」というサブクラスが「Living(生き物)」というスーパークラスを実装(implements)したいとき、
abstract class Material {} // 物質
class Air implements Material {} // 空気
class Water implements Material {} // 水
abstract class Living {
// Material(空気や海水など)を使って呼吸するメソッド
void breathe(Material material);
}
class Fish implements Living {
// コンパイルエラーを回避のため、スーパークラスと同型の引数
void breathe(Material material) {
...
}
}
というクラス設計をしたとします。
さて、これらをいざ用いようとしたとき、以下のような矛盾が生じてしまいます。
main() {
Fish().breathe(Water()); // OK
Fish().breathe(Air()); // 魚が空気で呼吸できるのはおかしい!
}
コード上はエラーなく実行できるかもしれませんが、オブジェクトの意味的には矛盾しています。
つまり、「事前条件が、基底クラス(スーパークラス)より強い」場合は矛盾が生じてしまいます。
ちなみに、Fish クラスの breathe メソッドにおける引数の定義部分で「最初から『Water』を渡せばいいじゃん!」と思い設計しようとしても、その場合には「スーパークラスのメソッドが十分にオーバーライドできていない」旨のコンパイルエラーが出ます。このことからも「引数(事前条件)により強い条件は持ってこれない」ことがわかります。
反対に「事前条件が、基底クラス(スーパークラス)と同等か、より弱い」場合、上記のような矛盾は起こらず安全であると言えます。
事後条件
仮に、上記の条件とは反対に「事後条件が、基底クラス(スーパークラス)より弱い」場合を考えます。
「Fish(魚)」や「Human(人間)」というサブクラスが「Living(生き物)」というスーパークラスを実装(implements)し、さらに、「Tuna(マグロ)」や「Salmon(サケ)」が「Fish(魚)」を実装したとき、
class Living {} // 生物
class Fish implements Living {} // 魚
class Tuna implements Fish {} // マグロ
class Salmon implements Fish {} // サケ
class Human implements Living {
Fish fish1st() { // 釣りその1
return Living() as Fish; // コンパイルエラーを防ぐための「as」;
}
}
というクラス設計をしたとします。
さて、Humanのfishメソッドを実行してみましょう。
main() {
Human().fish1st(); // エラー
}
実行してみると、「Living は Fish のサブタイプではない」という実行エラーが生じてしまいます。また、当然オブジェクトの意味的にも矛盾しています。生き物が釣れたなら、それが魚であるかは定かではありません。ライオンかも、地球外生命体であるかもしれません。
つまり、「事後条件が、基底クラス(スーパークラス)より弱い」場合は矛盾が生じてしまいます。
反対に以下のように、魚、特にマグロやサケが釣れる場合はエラーも生じず、意味的にも正しいです。
class Human implements Living {
Fish fish2nd() { // 釣りその2
return Tuna();
}
Fish fish3rd() { // 釣りその3
return Salmon();
}
}
main() {
final human = Human();
final catch2nd = human.fish2nd();
print(catch2nd);
final catch3rd = human.fish3rd();
print(catch3rd);
}
実行結果
Instance of 'Tuna' // マグロが釣れた
Instance of 'Salmon' // サケが釣れた
よって「事後条件が、基底クラス(スーパークラス)と同等か、より強い」場合は正しいと言えます。
不変条件
こちらは、基底クラス(スーパークラス)内で定められているルールを破るオブジェクトなど、当然ながら「is a」関係は成り立ちません。置換できません。全く異なる2つのオブジェクトとなります。
破ってしまった場合は?
以上のルールを破ってしまう継承関係や実装関係を考えたい場合は、もはや「is a」関係は成り立っていません。代わりに「has a」関係を築きましょう。具体的に以下のようにします。
✗「is a」関係: 継承、実装
class Sub extends[implements] Super {
//* フィールドやメソッドなど **//
}
↓ ↓ ↓
○「has a」関係: 移譲(コンポジション): つまり、引数で受け取ること
class Sub {
final Super param;
Sub(this.param);
//* 他フィールドやメソッドなど **//
}
おわりに
ご閲覧お疲れ様でした!
当人、聞きかじっただけの知識で書いておりますので、ご了承…。
コメントよろしくお願いします!(なんでも)
Discussion