今回はOpenClosedの続きの、Liskov Substitution(リスコフの置換原則)について見ていきます。
リスコフの置換原則
なんだか名前が難しそうですが、
「クラスから別のクラスを作ると、クラスが親になり、新しいクラスが子になり、子クラスは、親クラスができることをすべてできる必要があること」を意味します。
もっと簡単にいうと、親ができることは子どもに遺伝するということです。
お父さんが勉強と料理とスポーツができるなら、その子どもも全く同じようなことができるということです(現実的にはありえないですが...)
例えば、以下のコードだと、AnimalクラスをCatクラスが継承しています。
すると、Catはspeakというメソッドを使えるようになります。もしこれが使えないのであればリスコフの置換原則に反しているということになります。
この場合の、親をスーパークラス、子をサブクラスとも言います。
class Animal
def speak
puts "I am an animal"
end
end
class Cat < Animal
end
cat = Cat.new
cat.speak #=> "I am an animal"
しかしながら、リスコフの置換原則を守りすぎるとあまりにも不自由になってしまいます。
スーパークラスとは違うことがしたい!お父さんとは違うようになりたい!なんてことは大いにありえます。
医者の子どもが医者にならなくてもいいわけです(話が逸れました)
当然サブクラスをスーパークラスのインスタンスを期待しているところに渡すと壊れてしまいます。
ただ、そんな場合はそのようなインスタンスを渡さなければ良いだけです。
例えば、以下のように記述するとCatがAnimalを継承しているので、speakメソッドを使えるようになります。
しかし、Catクラスでもspeakメソッドを定義しているので、こちらがオーバーライドしています。
「猫だって話したい」という主張です。
class Animal
def speak
puts "I am an animal"
end
end
class Cat < Animal
def speak
## 後述だが、 "I am an animal"より弱い事後条件なのでNG
## 例えば、"I am an animal + α"などであればOK
puts "I am a cat"
end
end
def make_animal_speak(animal)
animal.speak
end
すると、引数にAnimalとCatのインスタンスを渡した場合、以下のような挙動になります。
つまり、子は親と同じ振る舞いをするというリスコフの置換原則に反していますね。
しかし、必要なら別にいいんです。
Rubyでは「プログラマーなら正しいやり方がわかるはず」という考え方を大切にしているらしいです。
したがって、あまりにも反しすぎるのはよくないにしても、本当にそれが必要ならば必ず守る必要がある訳ではないということです。
animal = Animal.new
cat = Cat.new
make_animal_speak(animal) #=> "I am an animal"
make_animal_speak(cat) #=> "I am a cat"
まとめるとオブジェクト指向設計では以下の通り。
正しい継承 = Is-a関係 + 振る舞いの同等性
利用者が想定していないバグが混入してしまう。
契約による設計
プログラムコードの中にプログラムが満たすべき仕様について記述することで、正確で頑健なソフトウェアとする設計技法
【利用する側】XXという条件でメソッドAを使わせてください・・・事前条件
・メソッド開始時に保証されるべき条件
・メソッドの引数、メソッドの開始時のインスタンスの状態
【利用される側】XXという条件でメソッドAを呼び出すならば、YYという結果を保証します・・・事後条件
・メソッド正常終了時に保証されるべき条件
・メソッド正常終了時のインスタンスの状態、クライアントに戻す返り値
Liskovの置換原理(LSP)に従うと、サブタイプは以下の条件を満たすべき
事前条件:
スーパタイプと同じかそれより弱い条件と置き換え
サブタイプのメソッドはスーパタイプのメソッドよりもさらに制約された条件を持つべきではありません。
つまり、サブクラスがスーパクラスの条件を包括していないといけないってこと。だって交換できなくなるから当然のことだよね。
事後条件:
スーパタイプと同じかそれより強い条件と置き換え
サブタイプのメソッドはスーパタイプのメソッドよりも緩い条件を持つべきではありません。
事前条件の例
Bad
class SuperClass {
hogeMethod(x) {
if (x > 0) {
return x * 2;
}
throw new Error("Invalid input");
}
}
class SubClass extends SuperClass {
hogeMethod(x) {
if (x > 10) {
return x * 2;
}
throw new Error("Invalid input");
}
}
Good
class SuperClass {
hogeMethod(x) {
if (x > 0) {
return x * 2;
}
throw new Error("Invalid input");
}
}
class SubClass extends SuperClass {
hogeMethod(x) {
if (x > -10) {
return x * 2;
}
throw new Error("Invalid input");
}
}
事後条件の例
Bad
class SuperClass {
hogeMethod(x) {
if (x > 0) {
return x * 2;
}
throw new Error("Invalid input");
}
}
class SubClass extends SuperClass {
hogeMethod(x) {
if (x > 0) {
return x;
}
throw new Error("Invalid input");
}
}
Good
class SuperClass {
hogeMethod(x) {
if (x > 0) {
return x * 2;
}
throw new Error("Invalid input");
}
}
class SubClass extends SuperClass {
hogeMethod(x) {
if (x > 0) {
return x * 2 * 2;
}
throw new Error("Invalid input");
}
}