リスコフの置換原則 とは?

リスコフの置換原則 (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 (正しい)

解決された問題

修正後のコードでは、SquareRectangleの振る舞いを壊すことなく、共通の親クラス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に違反していました。修正後は、支払い機能を持つかどうかの判定メソッドを共通化し、適切な動作を行うようにポリモーフィズムを活用しています。これにより、基底クラスの振る舞いを壊さずにリスコフの置換原則が守られています。