🤖

【SOLID原則】リスコフの置換原則 - LSP

2021/06/16に公開

SOLID原則とは、ソフトウェア設計の5つの原則の頭字語を取ったものです。ソフトウェアをより理解しやすく、より柔軟に、よりメンテナナンス性の高いものにするために考案されました。

  1. 単一責任の原則(Single Responsibility Principle)
  2. オープン・クローズドの原則(Open/closed principle)
  3. リスコフの置換原則(Liskov substitution principle)
  4. インターフェース分離の原則(Interface segregation principle)
  5. 依存性逆転の原則(Dependency inversion principle)

今回はSOLID原則のひとつ、リスコフの置換原則の原則についてです。

リスコフの置換原則

簡単に言うと、派生型(サブクラス)は、その基底型(スーパークラス)と置換可能でなければならないという原則です。

リスコフの置換原則について、書籍「アジャイルソフトウェア開発の奥義」では、このように書かれています。

原則: ここで望まれるのは、次に述べるような置換可能な性質である:S型のオブジェクトo1の各々に、対応するT型のオブジェクトo2が1つ存在し、Tを使って定義されたプログラムPに対してo2の代わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型であると言える。[1]

この引用の例を以下のコードに例えてみます。

  • S型のオブジェクトo1: Taxiクラス(サブクラス)
  • T型のオブジェクトo2: Carクラス(スーパークラス)
  • Tを使って定義されたプログラムP: driveメソッド
class Car {
    drive = () => 6
}
class Taxi extends Car {
    drive = () => '5時間'
}

const print = (arg) => {
    const driveTime = arg.drive()
    return `${driveTime}時間運転しました`
}

const car = new Car()
console.log(print(car)) // 6時間運転しました

const taxi = new Taxi()
console.log(print(taxi)) // 5時間時間運転しました ←日本語がおかしい

TaxiクラスはCarクラスをextendsで継承しています。継承の目的は、複数のクラスにあるメソッドやプロパティをスーパークラスに定義してコードの重複をなくすことです。

継承はis-aの関係(B is a Aは、BはAの一種である)が成立していなければいけません。今回のサンプルコードを見ると、タクシーは車の一種であるが成り立ちます。

しかし、リスコフの置換原則には違反しています。先程の引用の一部を振り返ります。

o2の代わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型であると言える。

o2の「Carクラス」の代わりにo1の「Taxiクラス」を使っても、driveメソッドの振る舞いがからない場合と言われていますが、サンプルコードでは振る舞いが変わっています。

それでは振る舞いが同じになるように、if文を追加してみましょう。

const print = (arg) => {
    const driveTime = arg.drive()
    if (car.constructor.name === 'Car') {
        return `${driveTime}時間運転しました`
    }
    if (car.constructor.name === 'Taxi') {
        return `${driveTime}運転しました`
    }
}

今度は、オープン・クローズドの原則にも違反してしまいました。

リスコフの置換原則(さらにオープン・クローズドの原則)を守るには、継承関係にあるクラスは振る舞いが変わらないようにします。

class Car {
    drive = () => 6
}
class Taxi extends Car {
    drive = () => 5
}

const print = (arg) => {
    const driveTime = arg.drive()
    return `${driveTime}時間運転しました`
}

const car = new Car()
console.log(print(car)) // 6時間運転しました

const taxi = new Taxi()
console.log(print(taxi)) // 5時間運転しました

今回はdriveメソッドの戻り値を揃えることで、スーパークラスとサブクラスが置換可能になりました。オープン・クローズドの原則も守ることができ、コードがシンプルになります。

脚注
  1. Robert C. Martin. アジャイルソフトウェア開発の奥義 ↩︎

Discussion