The Liskov Substitution Principle: Keep Your Promises

Warning: Illegal string offset 'filter' in /var/sites/t/ on line 1409

All programmers should be familiar with the five SOLID design principles. Along with Design Patterns, these are the most important things to understand for anyone wishing to design good (i.e. loosely coupled and highly cohesive) object-oriented software systems.

Of the five principles, the one which people have trouble grasping is the Liskov Substitution Principle.

This principle has been described in many ways by many people. Let’s begin with the technical and original description from Barbara Liskov, as referred to in Uncle Bob Martin’s paper on the subject:

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.

If you’re not familiar with the principle and you’re anything like me, this statement will make your head hurt.

A much better starting point is Uncle Bob’s interpretation from the same paper:

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

Or another description by Uncle Bob, from a podcast interview by Scott Hanselman:

The caller of the original entity [base class] should not be surprised by anything that happens if one of the subentities is substituted.

If I had to describe the principle in a sentence, it would be this:

Derived classes must keep any promises made by base classes.

Promises, Promises

So what are these promises that can be made by a base class?

To understand OO design principles and patterns, it is helpful to always think of your code as being part of an API, to be used by clients (i.e. programmers) who you have never met. Any property or method not declared as private is accessible to these clients, who might try to use your API in odd and unexpected ways. You obviously want to minimize the number of bug reports and support calls raised by them. So how can you do this?

One way would be to ship your API with detailed documentation, specifying how it should be used. What values it expects to be passed, and what exceptions it throws, for example. Your clients can’t see your code, they can only see the method signatures exposed by your API, but they can read your documentation to see how to use it in order to get the desired results.

A good approach for structuring this documentation would be to think in terms of Design By Contract, which demands the declaration of three types of promises for each method: preconditions, postconditions and invariants. Together these form a contract for each method.

Close-up image of a firm handshake standing for a trusted partnership

A method’s preconditions specify the conditions under which it promises to perform its task. As a simple example, consider a method which returns the square root of a number.

The precondition of this method might be that its argument is greater than or equal to zero. By specifying this precondition, the method relieves itself of all responsibility for what might happen if a negative number is passed to it.

A method’s postconditions specify what the world will look like after the method has completed. In the above example, the SquareRoot method might promise that it will not throw an exception, and it will return a value which is the square root of its argument, provided of course that its preconditions have been met.

The method’s invariants specify which properties it will not change during its execution. The SquareRoot method may promise as an invariant not to modify any properties exposed by any class.

Combining our preconditions, postconditions and invariants, the method makes the following promises to its users:

If you pass me a non-negative number, I promise to pass you back its square root, without changing the state of this or any other class. But if you pass me a negative number, I can’t promise anything.

Or to generalize the nature of a Design By Contract promise:

As long as these conditions are true, I promise to behave in this way.

Back to Liskov

Returning to the Liskov Substitution Principle, I said earlier I would describe it as follows:

Derived classes must keep any promises made by base classes.

The promises I am referring to here are the preconditions, postconditions and invariants defined, either explicitly or implicitly, by the base class.

To be more specific, in order to adhere to the principle, a derived class must follow 3 rules: it must not strengthen a precondition, it must not weaken a postcondition, and it must preserve invariants.

A condition is strengthened when it is made more restrictive, and it is weakened when it is made more lax.

In our square root example, if a derived class specified in its preconditions that the method will only behave as promised if the argument is greater than 10, then this would be a strengthened precondition and a violation of the LSP. The base class promised that it would handle any non-negative number, so the derived class is breaking the promise made by the base class.

However, if the precondition was weakened, by specifying that negative numbers are in fact allowed, and will be handled by the throwing of a specific type of exception, then this is not a violation. The promise made by the base class – that it would handle any non-negative number – still holds true.

The Square-Rectangle Example

One example commonly used in describing the LSP is that of the square and the rectangle.

A programmer observes that a square is a type of rectangle, and so defines a Square class which inherits from the Rectangle class. This seems reasonable. However, the problem arises when we consider a method, SetHeight, which exists in both the Rectangle and Square classes.

In the Rectangle class, one of the invariants of the SetHeight method is that it will not modify the class’s Width property. But in the Square class, it seems perfectly correct for the SetHeight method to set both its Height and Width properties. In this way the LSP is violated, as the derived class does not preserve the invariants specified by the base class.

The Electric Duck Example

A second common example is that of the electric duck. It may seem fair to conclude that an electric duck is a type of duck, and therefore the class ElectricDuck should inherit from Duck. However, consider a Swim method  defined in both classes. In the Duck class, the Swim method has a postcondition which promises to always set a property, IsSwimming, to true. However, in the ElectricDuck class, the IsSwimming property is only set to true if a separate property, IsTurnedOn, is true. In this way the method’s postcondition is weakened. The promise of always setting IsSwimming to true has been broken.

A Word on Inheritance

The Liskov Substitution principle relates to inheritance, which is one of the three standard features of an object-oriented programming language, the other two being encapsulation and polymorphism. I learnt about these three OO features at university, so I was surprised as a junior programmer to read, in Head First Design Patterns, about the dangers of using inheritance in systems design, and of the Composition Over Inheritance principle. The square-rectangle and electric duck examples show that the commonly cited “is-a” rule for inheritance can be problematic. A square is a rectangle, and it can be argued that an electric duck is a duck (depending on how you define a duck). But as we can see, structuring your program according to these relationships can be problematic. Inheritance can be extremely useful, but some programmers are too eager to use it. It should be used with caution.

Final Thoughts

Like all of the SOLID design principles, I see the Liskov Substitution Principle as a principle of integrity and respect. Imagine that your public methods and properties will be used by other programmers, and design your code so that they will not encounter any nasty surprises. Noone likes calling a method only to find it causes some unexpected behaviour. Save future programmers, including yourself, the headache of debugging and ensure your code keeps its promises.


Share Button