きれいなコードを書くためにSOLID原則を学びました③ ~リスコフの置換原則~


今回はSOLIDのリスコフの置換原則についてまとめました。

その他の記事は以下。
きれいなコードを書くためにSOLID原則を学びました① ~単一責任の原則~
きれいなコードを書くためにSOLID原則を学びました② ~オープン・クローズドの原則~
きれいなコードを書くためにSOLID原則を学びました④ ~インターフェース分離の原則~
きれいなコードを書くためにSOLID原則を学びました⑤ ~依存関係逆転の原則~

リスコフの置換原則

Object should be replaceable with their subtypes without affecting the correctness of the program

これを直訳すると、「オブジェクトは、プログラムの正しさに影響を与えずにそのサブタイプと交換可能であるべきである。」という意味になります。

サブタイプは、親クラスを継承したクラスのことをいいます。要は、[サブクラス][親クラス]であるという関係のことですね。
ex.) コンパクトカーである、ダチョウである

Bird(鳥)クラスとBirdクラスを継承したOstrich(ダチョウ)クラスを考えてみます。

public class Bird {
  public void fly() {
    // Fly high!
  }
}

public class Ostrich extends Bird {
  public void fly() {
    // Unimplemented
    throw new RuntimeException();
  }

  public void run() {
    // Run fast!
  }
}

継承により、OstrichクラスはBirdクラスのメソッドをそのまま使うことができるのですが、ダチョウは空を飛ぶことができないので、flyメソッドを例外として上書きしています。

以下で、Birdクラスのインスタンスを2つ、Ostrichクラスのインスタンスを1つつくって、flyメソッドを出力するようなBirdHabitsというクラスを考えてみます。

public class BirdHabits {
  public static void main(String[] args) {
    Bird first = new Bird();
    Bird second = new Bird();
    Bird third = new Ostrich();
  }

  List<Bird> myBirds = new ArrayList<>();
  myBirds.add(first);
  myBirds.add(second);
  myBirds.add(third);

  for(Bird bird: myBirds) {
    System.out.println(bird.fly());
  }
}

Ostrichクラスからつくられたthirdインスタンスのflyが呼び出されるときだけ、例外処理が走ります。

ここで親クラスをサブクラスと交換してみましょう。
Ostrichクラスのインスタンスを

Bird third = new Ostrich();

Birdクラスのインタンスに置き換えたらどうなるでしょうか?

Bird third = new Bird();

以下の部分で3回ともBirdのflyメソッドが実行されてしまい、クラスを置き換える前とプログラムのふるまいがかわってしまいます。

for(Bird bird: myBirds) {
  System.out.println(bird.fly());
}

したがって、以上の例は、リスコフの置換原則に違反しています。
このように、ダチョウ(Ostrich)は鳥(Bird)であることは確かなのですが、鳥から継承できないふるまい(メソッド)が含まれている限り、リスコフの置換原則的には望ましくない設計であるということになります。

解決法: さらに一般的な親クラスを考える

それでは、どうすれば原則を満たす理想的な設計となるのでしょうか。
解決法は、更に一般的な親クラスを用意してあげることです。

今までの例で考えれば、「鳥もダチョウも動物である」のでAnimal(動物)クラスというものを新しく用意してあげることになります。

Animalクラスでは新しくmoveメソッドというものをつくり、継承先のクラス(Bird, Ostrich)でそれぞれのメソッド(fly, run)を返してあげます。

public class Animal {
  public void move() {
    // move
  }
}

public class Bird extends Animal {
  public void move() {
    return this.fly();
  }
  public void fly() {
    // Fly high!
  }
}

public class Ostrich extends Bird {
  public void move() {
    return this.run();
  }

  public void run() {
    // Run fast!
  }
}

こうすることで、AnimalHabitsクラスの中でつくったインスタンスがBirdクラスのものであろうとOstrichクラスのものであろうと、moveメソッドは問題なく呼び出されます。

public class AnimalHabits {
  public static void main(String[] args) {
    Animal first = new Bird();
    Animal second = new Bird();
    Animal third = new Ostrich();
  }

  List<Animal> myAnimals = new ArrayList<>();
  myAnimals.add(first);
  myAnimals.add(second);
  myAnimals.add(third);

  for(Animal animal: myAnimals) {
    System.out.println(animal.move());
  }
}

これで原則を満たすような設計になりました。

おわりに

むやみやたらと継承をつかうと痛い目をみる可能性があるという話ですね。今後気をつけます。

参考書籍・動画

Crean Architecture Robert.C.Martin
SOLID Principles: Introducing Software Architecture & Design Sujith George