11.5. Liskov Substitution Principle¶
The Liskov Substitution Principle (LSP) is named for Turing Award winner Barbara Liskov, who did seminal work on subtypes that heavily influenced object-oriented programming. Informally, LSP states that a method designed to work on an object of type \(T\) should also work on an object of any subtype of \(T\) . That is, all of \(T\)’s subtypes should preserve \(T\)’s “contract.”
1class Rectangle
2 attr_accessor :width, :height, :top_left_corner
3 def new(width,height,top_left) ... ; end
4 def area ... ; end
5 def perimeter ... ; end
6end
7# A square is just a special case of rectangle...right?
8class Square < Rectangle
9 # ooops...a square has to have width and height equal
10 attr_reader :width, :height, :side
11 def width=(w) ; @width = @height = w ; end
12 def height=(w) ; @width = @height = w ; end
13 def side=(w) ; @width = @height = w ; end
14end
15# But is a Square really a kind of Rectangle?
16class Rectangle
17 def make_twice_as_wide_as_high(dim)
18 self.width = 2*dim
19 self.height = dim # doesn't work!
20 end
21end
This may seem like common sense, but it’s subtly easy to get wrong. Consider the code
in Figure 11.16, which suffers from an LSP violation. You might think a Square
is just a
special case of Rectangle
and should therefore inherit from it. But behaviorally, a square
is not like a rectangle when it comes to setting the length of a side! When you spot this
problem, you might be tempted to override Rectangle#make_twice_as_wide_as_high
within Square
,
perhaps raising an exception since this method doesn’t make sense to call on a Square
. But
that would be a refused bequest—a design smell that often indicates an LSP violation. The
symptom is that a subclass either destructively overrides a behavior inherited from its
superclass or forces changes to the superclass to avoid the problem (which itself should
indicate a possible OCP violation). The problem is that inheritance is all about implementation
sharing, but if a subclass won’t take advantage of its parent’s implementations, it might not
deserve to be a subclass at all.
The fix, therefore, is to again use composition and delegation rather than inheritance, as
Figure 11.17 shows. Happily, because of Ruby’s duck typing, this use of composition and
delegation still allows us to pass an instance of Square
to most places where a Rectangle
would be expected, even though it’s no longer a subclass; a statically-typed language would
have to introduce an explicit interface capturing the operations common to both Square
and
Rectangle
.
1# LSP-compliant solution: replace inheritance with delegation
2# Ruby's duck typing still lets you use a square in most places where
3# rectangle would be used - but no longer a subclass in LSP sense.
4class Square
5 attr_accessor :rect
6 def initialize(side, top_left)
7 @rect = Rectangle.new(side, side, top_left)
8 end
9 def area ; rect.area ; end
10 def perimeter ; rect.perimeter ; end
11 # A more concise way to delegate, if using ActiveSupport (see text):
12 # delegate :area, :perimeter, :to => :rectangle
13 def side=(s) ; rect.width = rect.height = s ; end
14end
Self-Check 11.5.1. Why is Forwardable
in the Ruby standard library provided as a
module rather than a class?
Modules allow the delegation mechanisms to be mixed in to any class that wants to use them, which would be awkward if
Forwardable
were a class. That is,Forwardable
is itself an example of preferring composition to inheritance!