リスコフの置換原則 とは?
リスコフの置換原則 (Liskov Substitution Principle, 略称LSP) は、オブジェクト指向設計の原則の一つであり、次のように説明できます。
「派生クラスは常にその基底クラスと置き換え可能でなければならない。」
つまり、サブクラスはスーパークラスの振る舞いや期待を壊さずに代替可能であるべきであり、サブクラスがスーパークラスの動作を変更したり、矛盾する動きをしてはいけないという原則です。
なぜ リスコフの置換原則 が重要なのか?
この原則が守られていないと、ポリモーフィズムの利点が損なわれ、コードの柔軟性が失われます。リスコフの置換原則を守ることで、クラスやオブジェクトが互換性を持ち、コードの再利用性が向上し、変更に強い柔軟な設計が可能になります。
以下、具体的なコード例を通じて、実際にどのような問題が起こり、リスコフの置換原則を守ることでそれがどのように解決されるのかを見ていきましょう。
修正前のコード
例えば、次のようなコードを考えてみましょう。
class Rectangle
attr_accessor :width, :height
def area
@width * @height
end
end
class Square < Rectangle
def width=(value)
@width = @height = value
end
def height=(value)
@width = @height = value
end
end
def resize_and_calculate_area(rectangle)
rectangle.width = 10
rectangle.height = 5
rectangle.area
end
rectangle = Rectangle.new
square = Square.new
puts resize_and_calculate_area(rectangle) # => 50 (正しい)
puts resize_and_calculate_area(square) # => 25 (予期しない結果)
問題点
上記のコードでは、Square
クラスはRectangle
を継承していますが、挙動が異なります。Square
は幅と高さが常に同じですが、resize_and_calculate_area
メソッドは長方形を前提にしているため、正方形の場合は予期しない結果が起こります。これはリスコフの置換原則に違反している典型的な例です。
修正後のコード
リスコフの置換原則に従えば、正方形と長方形を互換性がある形で設計するには、共通のインターフェースを持たせ、継承ではなくポリモーフィズムを利用します。
class Shape
def area
raise NotImplementedError
end
end
class Rectangle < Shape
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
@width * @height
end
end
class Square < Shape
attr_accessor :side
def initialize(side)
@side = side
end
def area
@side * @side
end
end
def calculate_area(shape)
shape.area
end
rectangle = Rectangle.new(10, 5)
square = Square.new(5)
puts calculate_area(rectangle) # => 50 (正しい)
puts calculate_area(square) # => 25 (正しい)
解決された問題
修正後のコードでは、Square
はRectangle
の振る舞いを壊すことなく、共通の親クラスShape
を経由してポリモーフィズムを活用しています。これにより、各クラスが持つ固有の特性を維持したまま、互換性を保つことができ、予期しない結果がなくなりました。
まとめ
リスコフの置換原則を守ることで、オブジェクト指向設計における継承の誤用を防ぎ、変更に強く柔軟で信頼性のあるシステムを構築できます。継承よりインターフェースや抽象クラスを使ったポリモーフィズムを適切に利用することで、よりエレガントな解決策が可能となります。
練習問題 (1)
以下のコードは、リスコフの置換原則に違反しています。修正してください。
修正前のコード
class Bird
def fly
"飛んでいます"
end
end
class Penguin < Bird
def fly
raise "ペンギンは飛べません!"
end
end
def let_bird_fly(bird)
bird.fly
end
bird = Bird.new
penguin = Penguin.new
puts let_bird_fly(bird) # => 飛んでいます
puts let_bird_fly(penguin) # => 例外発生
修正後のコード
class Bird
def move
"移動しています"
end
end
class FlyingBird < Bird
def move
"飛んでいます"
end
end
class Penguin < Bird
def move
"泳いでいます"
end
end
def let_bird_move(bird)
bird.move
end
bird = FlyingBird.new
penguin = Penguin.new
puts let_bird_move(bird) # => 飛んでいます
puts let_bird_move(penguin) # => 泳いでいます
修正前のコードでは、ペンギンが予期しない例外を発生させ、基底クラスであるBirdの振る舞いを破壊していました。修正後は、共通のメソッド名を用いて各クラスが適切な動作をするようになり、リスコフの置換原則が守られています。
練習問題 (2)
以下のコードはLSPに違反しています。問題を修正しましょう。
修正前のコード
class Payment
def pay(amount)
"¥#{amount}を支払いました"
end
end
class FreePayment < Payment
def pay(amount)
raise "無料ユーザーは支払えません!"
end
end
修正後のコード
class User
def available_payment_method?
false
end
end
class PaidUser < User
def available_payment_method?
true
end
def pay(amount)
"¥#{amount}を支払いました"
end
end
def process_payment(user, amount)
if user.available_payment_method?
user.pay(amount)
else
"無料ユーザーは支払えません"
end
end
free_user = User.new
paid_user = PaidUser.new
puts process_payment(free_user, 1000) # => 無料ユーザーは支払えません
puts process_payment(paid_user, 1000) # => ¥1000を支払いました
修正前はサブクラスが基底クラスの振る舞いを破壊し、LSPに違反していました。修正後は、支払い機能を持つかどうかの判定メソッドを共通化し、適切な動作を行うようにポリモーフィズムを活用しています。これにより、基底クラスの振る舞いを壊さずにリスコフの置換原則が守られています。