The Liskov Substitution Principle

The Liskov Substitution Principle (or LSP) is one of the SOLID Principles, originally expressed by Barbara Liskov in 1988[BL1].
Although the authoritative sources express it in more formal language (see “Formally” below), it may be summarized as:

Clients of a Supertype S should be able to use instances of a Subtype T, without knowing it, and without undesired results.

Interestingly, the LSP is a Semantic principle, rather than a Syntactic one: as we can see in some examples (eg: “The Square/Rectangle Problem” below) the syntax of a language may be respected while breaking the LSP.

Square and Rectangle

Formally

The LSP has been formally described by Barbara Liskov in 1998[BL1] as follows:

What is wanted here is something like the following substitution property:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Later, Uncle Bob paraphrased it[RM1]:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

The Square/Rectangle Problem

The classic example used to illustrate the problem of a system violating the LSP is that of the Square and Rectangle classes (or, similarly, of the Circle and Ellipse classes[WK1]. Imagine a system that should handle geometric shapes, among which Squares and Rectangles, to perform a variety of operations (eg: painting them, calculating geometric properties, and so on…).
At first approximation, we could implement these classes using an inheritance relationship; after all, in geometry, a Square is but a specific case of the more generic Rectangle shape.

class Rectangle
{
  protected int width = 0;
  protected int height = 0;

  public virtual int Width
  { get { return width; } set { width = value; } }

  public virtual int Height
  { get { return height; } set { height = value; } }
}

class Square : Rectangle
{
  public override int Width
  { set { width = value; height = value; } }

  public override int Height
  { set { width = value; height = value; } }
}

In other words, we made Square a subtype of Rectangle, and overrode the setters for the two dimensions, to ensure that, when either is set to a new value, the other is set to the same value. This appears resonable, and allows us to write clients that use the Rectangle Supertype even when a Square is provided:

class Client
{
  void DoSomething(Rectangle rectangle)
  { /*...do something..*/ }
}

//...and in our Main method:
var rectangle = new Rectangle();
Client.DoSomething(rectangle);

var square = new Square();
Client.DoSomething(square);

While the syntactic rules of the language are respected by this example, we’ve already violated the semantic expectation of a reasonable client, or, in other words, we’ve violated the LSP. To demonstrate the problem, let’s rename the Client.DoSomething method, and reveal its implementation:

class Client
{
  void PrintArea(Rectangle rectangle)
  {
    var area = rectangle.Width*rectangle.Height;
    Console.WriteLine("Area = "+area);
  }
}

//...and in our Main method:
var rectangle = new Rectangle();
rectangle.Width=4;
rectangle.height=5;
Client.PrintArea(rectangle); //Prints 20

var square = new Square();
square.Width=4;
square.Height=5;
Client.PrintArea(square); //Prints 25

The problem should be apparent by now: a reasonable Client, given a Rectangle instance, would not expect the different behavior we built in the Square subtype, where the dimensions are constrained to each other, and the order in which we set them suddenly becomes critical.

If the example seems contrived, consider that the code generating the Square and/or rectangle instances, setting their dimensions, and passing the object to the Client.PrintArea method would probably be very distant from each other. In a non-trivial modern system, those line of codes might not even run on the same machine.

The Underlying Problem

As suggested by Uncle Bob[RM1], the example demonstrates a problem with the design of the system: the assumptions made by the designers/developers of the Square and Rectangle classes don’t match the expectations of the developers consuming them in the Client. Which leads us to recognize an important aspect:

Validity is not Intrinsic
A model, viewed in isolation, can not be meaningfully validated. The validity of a model can only be expressed in terms of its clients.

The Immutable Solution

One solution to class hierarchies violating the LSP is that of implementing the classes involved as Immutable Objects. For instance, the Square and rectangle classes can be converted as follows:

class Rectangle
{
  protected int width = 0;
  protected int height = 0;

  public int Width { get { return width; } }
  public int Height { get { return height; } }

  public Rectangle(int width, int height)
  {
    this.width=width;
    this.height=height;
  }
}

class Square : Rectangle
{
  public Square(int dimension)
    : base(dimension, dimension)
  { }
}

Intriguingly, in this solution, the code in the Square class has disappeared. This is because the only code in it was the one dealing with the Square-specific behavior of modifying both dimensions when either is changed – but for an immutable class this behavior is unacceptable. A Client would have to instantiate a new Square (or Rectangle, for that matter) when a new dimension (or pairing thereof) is necessary:

class Client
{
  void PrintArea(Rectangle rectangle)
  {
    var area = rectangle.Width*rectangle.Height;
    Console.WriteLine("Area = "+area);
  }
}

//...and in our Main method:
var rectangle = new Rectangle(4, 5);
Client.PrintArea(rectangle); //Prints 20

var square = new Square(5);
Client.PrintArea(square); //Prints 25

Composition Over Inheritance

The example used to demonstrate the LSP is also often cited as an example to demonstrate the preference for Composition Over Inheritance, another principle (or advice) for Good Object Oriented Design. Sometimes this is referred to as the “IS-A vs HAS-A” problem since the design issue seems to stem from the fact that the Square inherits from (“IS-A”) Rectangle.
However, it is important to note that the violation of the LSP is actually caused by a mismatch between the assumptions made by the designer of the Square and Rectangle class, and the expectations of the designer of the Client system. The IS-A relationship is not implicitly ‘at fault’.

Solutions

When facing a LSP violation, there are a variety of options for alternative solutions. Among them:

  • The Square HAS-A Rectangle, which it instantiates with identical width and height, it modifies when the Square dimension is changed, and it uses for any shared behavior (eg: Calculating the Area)
  • The Square and Rectangle both use a (new) third component for their shared behavior (eg: an AreaCalculator)
  • The Square drops its inheritance from Rectangle, but also adds a 'View' method (eg: Square.AsRectangle() ) so that it can easily be passed to a Client expecting a Rectangle
  • Drop the Square class, using only the rectangle class (of course this forces all Clients to enforce the Square-specific logic on their own)

See [WK1] for a more exhaustive list.

References:

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s