RIT Tech Blog

株式会社RITのエンジニアが知見を共有する技術ブログです。

リスコフの置換原則とは何か

エンジニアの前田です。

SOLIDの原則のLにあたる「リスコフの置換原則」を調べてみました。

概要

リスコフの置換原則とは、 is a関係にあるクラスを定義する時に、サブタイプのオブジェクトはスーパータイプのオブジェクトの仕様に従わなければならない、という原則です。

守るべきこと

リスコフの置換原則では、以下の項目(事前条件・事後条件・不変条件)を順守すべきであるとされています。

事前条件

・事前条件を、サブクラスで強めることはできない。サブクラスでは同じか弱められる。

事前条件とはある操作をする際に満たさなければいけない条件のことです。

事後条件

・事後条件を、サブクラスで弱めることはできない。サブクラスでは同じか強められる。

事後条件とはある処理が実行された後に成立する条件のことを言います。

不変条件

・不変条件は、派生型でも保護されねばならない。派生型でそのまま維持される。

propaty Aのとりうる値が 1~100の間であれば、サブクラスでもそれを守らなければいけません。

原則に逆らうと何が起こるのか

リスコフの置換原則に逆らうと、サブクラスが使いづらくなってしまいます。 全てのサブクラスを同様に処理することがしにくいため、リスコフの置換原則に従っていないサブクラスを増やすごとに、サブクラスを使っている箇所で修正が必要になったりします。 結果的にオープンクローズドの原則にも反する実装になってしまいますね。オープンクローズドの原則はこちらの記事を参照お願いします。 オープンクローズドの原則とは、簡単に説明すると、機能追加や修正の際に対象の箇所以外の既存のソースコードを変更する必要がないプログラムにしましょうということです。

以下によくない例として疑似コードで例を示します。

class{
  private ingredients: 材料[];
  constructor(public ingredients: 材料[]) {
    this.ingredients = ingredients;
  }
  
  public boil() {
    this.ingredients.map(ingredient => {
      return ingredient.boil();
    })
  }
}

// スーパークラス
abstract class 材料 {
  abstract public boil(): void;
}

// サブクラス1
class 枝豆 extend 材料  {
    public boil() {
       // 色々な処理
    }
  }
}

// サブクラス2
class 人参 extend 材料 {
    private isPrepareBoid: boolean = false;
    public prepareBoid() {
      // boidする前に必要な処理

      this.isPrepareBoid = true
    }

    public boil() {
       if (!this.isPrepareBoid) throw new Error('茹でる前の準備がされていません');
       // 色々な処理
    }
  }
}

const edamame = new 枝豆();
const carrot = new 人参();
const pot = new([edamame, carrot]);
pot.boil(); // carrotでprepareBoidが実行されていないのでエラーになってしまう!

この例では、スーパークラスとして材料クラス、サブクラスとして枝豆クラスと人参クラスを実装しています 枝豆クラスは材料クラスと置換可能なように実装されているため問題ないですが、 人参クラスはboilメソッドの前に必要な処理として、材料クラスには存在しないメソッドであるprepareBoilを実装していて、boilの前にprepareBoilを実行しないとエラーになってしまうため、boidメソッドを実行した時にエラーになってしまっています。 人参クラスのboilメソッドの事前条件が材料クラスのboilメソッドと違い、prepareBoilの実行後を前提としているため発生しています。

鍋クラスのboilメソッド内でエラーが発生しないように実装するとしたらboilメソッドを以下のように修正するでしょうか

  public boil() {
    this.ingredients.map(ingredient => {
      if (instansof ingredient === '人参') {
        ingredient.prepareBoid();
      }
      return ingredient.boil();
    })
  }

ingredientのboilメソッドを実行する前に、ingredientが人参クラスのインスタンスの場合は、prepareBoilを実行するようにしました。 一応エラーは出なくなりましたが、人参クラスのようなboilメソッドの事前条件が材料クラスと違うサブクラスを作成するたびに鍋クラスを修正しなくてはいけなくなりましたね これは修正に対して閉じていないという点で、オープンクローズドの原則に違反しています。

良い例

材料クラスにprepareBoidメソッドを追加しましょう

abstract class 材料 {
  private isPrepareBoil: boolean = false;
  abstract public boil(): void;

  public prepareBoil() {
    this.isPrepareBoil = true;
  }
}

class 人参 extend 材料 {
    public prepareBoil() { // オーバーライドする
      ...
    }

    ...
  }
}

class{
  private ingredients: 材料[];
  constructor(public ingredients: 材料[]) {
    this.ingredients = ingredients;
  }
  
  public boil() {
    this.ingredients.map(ingredient => {
      ingredient.prepareBoil(); // 全てのIngredientインスタンスにprepareBoilが実装されているので場合分けしなくて良い
      return ingredient.boil();
    })
  }
}

const edamame = new 枝豆();
const carrot = new 人参();
const pot = new([edamame, carrot]);
pot.boil(); // 正常に実行が完了する

prepareBoilを材料クラスに追加し、材料クラスのサブクラスで共通で実行することに変更した結果、サブクラスごとに場合分けをしなくてもよくなりました。  リスコフの置換原則に従った効果です!