🐡

「リスコフの置換原則」における簡単な理解

2023/07/05に公開

「リスコフの置換原則」とは簡単に言うと?

「リスコフの置換原則」とは、簡単に言うと、「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