リスコフの置換原則(Liskov Substitution Principle)
リスコフの置換原則(LSP)は、次のように要約されている:
派生型はその基本型と置換可能でなければならない。
アジャイルソフトウェア開発の奥義 第2版 p.143-144
簡単にいえば、派生クラスは基本クラスと互換性がなければならないということである。
適当な計算を行うクラスがあるとしよう。
class Calculator
def calculation(value)
if value.is_a?(Integer)
raise ArgumentError.new('整数以外は指定できません!')
end
result = calc # いろんな計算をしてresult変数に格納する
return result.to_i
end
end
このCalculator#calculationメソッドは引数としてIntegerの値を受け取る。また、結果としてArgumentError例外が発生するかIntegerの値を返す。
このクラスを利用しているコードは以下のようになるだろう。
# 引数のcalculatorはCalculatorかその派生クラスのインスタンス
def foo(calculator, value)
result = calculator.calculation(0) + calculator.calculation(value)
# resultがIntegerであることに依存したコード...
rescue ArgumentError => e
# 例外処理
end
派生クラスとして以下の場合を考えてみる。
class CalculatorA < Calculator
def calculation(value)
# Integer以外なら0扱い
value = 0 unless value.is_a?(Integer)
result = calc # いろんな計算をしてresult変数に格納する
return result.to_i
end
end
このCalculatorA#calculationメソッドは引数として任意の値を受け取る。また、結果として例外は発生することなくIntegerの値を返す。
このCalculatorAクラスのインスタンスは、先ほどのfooメソッドを改修することなくfooメソッドに渡すことができる。
これは、基本クラスに対して派生クラスの入力の範囲が広がり(Integerだけから任意の値になった)、出力の範囲が狭まっている(ArgumentError例外が発生しなくなった)ためである。
逆に以下のような派生クラスを考えてみる。
class CalculatorX < Calculator
def calculation(value)
if value.is_a?(Integer)
raise ArgumentError.new('整数以外は指定できません!')
end
# いろんな計算をしてresult変数に格納する
result = 100 / value # ゼロ除算の可能性がある
return result.to_i
end
end
このCalculatorX#calculationメソッドは引数として0以外のIntegerの値を受け取る。また、結果としてArgumentError例外かZeroDivisionError例外が発生するかIntegerの値を返す。
このCalculatorXクラスのインスタンスは、先ほどのfooメソッドを改修しなければfooメソッドに渡すことができない。必ず、ZeroDivisionError例外が発生してしまうし、ZeroDivisionError例外の捕捉をしていないためプログラムが異常終了してしまうだろう。
CalculatorXクラスの場合はZeroDivisionError例外が増えるという形で出力が増えているが、コード値を返すメソッドでコード値の種類が増えるようなケースでも同じである。例えば、基本クラスではA、B、Cのいずれかしか返さないのに、派生クラスでXも返すようになると次のコードは壊れてしまう。
def bar(object)
case object.code
when 'A'
# ...
when 'B'
# ...
when 'C'
# ...
end
end
まとめ
- リスコフの置換原則とは簡単にいえば、派生クラスは基本クラスと互換性がなければならないということ
- 基本クラスに対して派生クラスの入力の範囲が同じか広がり、出力の範囲が同じか狭まっているならリスコフの置換原則に則っている
リスコフの置換原則を順守することは必須である。リスコフの置換原則に違反したコードはその時点で不具合が発生しているか、不具合のタネを抱えてしまっている。