リスコフの置換原則(LSP)

LSP

サブクラスは、そのスーパークラスで代用可能でなければならない。という原則です。
理解しやすいためにたとえ話を作ってみました。

人で考えてみます。私はiosのアプリ開発をしているプログラマーです。
しかし、iosアプリ開発者の手が足りません。
ここでいい加減に「私」のスーパークラスを考え、同じ親から生まれた(子クラス)弟に開発の一部を頼むことにしました。
同じ親から生まれた弟ならば同じ機能を使えると思ったわけです。

しかし結果はどうでしょうか、もちろんできるわけがありません。これは弟が悪いのではなく、置き換え出来ないものを置き換えてしまった私の責任です。

具体例

例を満たすためには画像のようにクラスAがB1のインスタンスに依存している時、B2を渡しても同じように動作しなければなりません。


間違った例をコードに起こすとこのような形になります。

class human {
    func ability(){
        print("逆立ちできるよ")
    }
}

class B:human {
    override func ability() {
        print("iosアプリが作れるよ!")
    }
}

class B2: human {
    override func ability() {
        print("魚を捌けるよ")
    }
}

例の考え方重視した際humanクラスの持っているabilityメソッドは親と同じものを持つべきです。

Swiftではfinal演算子が使えるので親のabilityを変更させなければ良いのです。
finalで定義しなおすと以下のようになります。

class human {
    final func ability(){
        print("逆立ちできるよ")
    }
}

class B:human {
}

class B2: human {
}

無理に変更を加えようとすると画像のようにerrorになります。

Languageの例

もう一つ例を考えてみます。

releaseYearはLanguageクラスを継承した各子クラスの発表年を表示するメソッドです。

このメソッドは、LSPの原則とOCPの原則に違反してしまっています。全てのLanguage型を知りそれぞれのクラスで実装したメソッドを呼び出しています。新規にLanguageを追加するたびに関数の中身を変更する必要があります。

func releaseYear(languages:[Language]){
    languages.forEach { (lang) in
        if lang.name == "C" {
            repeaseYearC(lang)
        }
        if lang.name == "Swift" {
            repeaseYearSwift(lang)
        }
        if lang.name == "Kotlin" {
            repeaseYearKotlin(lang)
        }
    }
}

この関数をLSPの原則に従わせる為Steve Fentonが唱えた以下の要求事項に従います。

・スーパークラス(Animal)がスーパークラス型の(Animal)パラメータを受け入れるメソッドを持っている場合、そのサブクラス(Pigeon)も引数として、スーパークラス型(Animal type)あるいは、サブクラス型(Pigeon type)を受け入れなければならない。
・スーパークラスがスーパークラス型(Animal)を返す場合、そのサブクラスはスーパークラス型(Animal type)あるいは、サブクラス型(Pigeon)を返さなければならない。

https://postd.cc/solid-principles-every-developer-should-know/

SupreClassであるLanguageを以下のように修正します。
finalでreleaseYearメソッドを定義したので、Languageを継承している子クラスは全て同じ動きをするようになります。

class Language {
    let birthYear:Int
    
    init(birthYear: Int) {
        self.birthYear = birthYear
        
    }
    final func releaseYear(){
        print(self.birthYear)
    }
}

呼び出し元も以下のように新たにLanguaheのインスタンスが追加されてもメソッド内を修正することなく処理を行うことができます。

let language: [Language] = [C(birthYear: 1972), Swift(birthYear: 2014), Kotlin(birthYear: 2011)]

func releaseYear(languages:[Language]){
    languages.forEach({
        $0.releaseYear()
    })
}

参考文献

オブジェクトで思考すると見えてくる、自由な世界-リスコフの置換原則

開発者が知っておくべきSOLIDの原則