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
Figure 11.16: Behaviorally, rectangles have some capabilities that squares don’t have—for example, the ability to set the lengths of their sides independently, as in Rectangle#make_twice_as_wide_as_high.

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
Figure 11.17: As with some OCP violations, the problem arises from a misuse of inheritance. As Figure 11.18 shows, preferring composition and delegation to inheritance fixes the problem. Line 12 shows a concise syntax for delegation available to apps using ActiveSupport (and all Rails apps do); similar functionality for non-Rails Ruby apps is provided by the Forwardable module in Ruby’s standard library.
11.18
Figure 11.18: Left: The UML class diagram representing the original LSP-violating code in Figure 11.16, which destructively overrides Rectangle#make_twice_as_wide_as_high. Right: the class diagram for the refactored LSP-compliant code in Figure 11.17.

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!