Aaron Proctor

The incoherent braindump of a software developer

Liskov Substitution Principle

This is part 3 of a series of blog posts on the SOLID principles – five fundamental principles of object-oriented software design.

The Liskov Substitution Principle

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

Following the Open-Closed principle invariably results in inheritance hierarchies. While this is not automatically a problem, ill-considered and frivolous use of inheritance can cause more problems than it solves.

The Liskov Substitution Principle can help us think about how to build inheritance trees whilst maintaining a consistent object model and adhering to the Open/Closed principle.

The classic and oft-touted example of an LSP violation is the Rectangle class:

Let’s imagine that due to some additional requirements, we need to be able to work with squares as well as rectangles. We know that in Object Oriented design, an inheritance signifies an “is a” relationship – and clearly a square “is a” rectangle, mathematically.

This might seem appropriate but it quickly starts to break down. It doesn’t make sense for us to be able to set the width and height of the square separately, yet in this case we can. Of course we can add logic to the properties in the Square class:

But what happens when we pass a Square into this client function?

We’re left with a square with different-length sides. Instead of shadowing the properties, we could declare them virtual and override them (leaving aside that this means changing the Rectangle class, violating OCP).

This would prevent this issue but is not really addressing the problem: the contract of the rectangle class no longer applies to a square. Setting the height of a rectangle should not affect the width, and if a developer has written code which depends on these conditions it will not function properly with a Square. This is a violation of the Open/Closed Principle – functions which worked before the changes no longer work without modification.

The problem here is in the initial assumption. A Square object is not a Rectangle object, even though in the real world we know that a square is a rectangle. The data model is the same – a rectangle and a square both have a height and a width – but the behaviour of a rectangle does not apply to a square. This is the fundamental lesson of LSP – a derivative must conform to the behaviour that clients expect of the base classes they use.

How does this apply to the real world? Let’s take another look at our game. A developer implements a new type of enemy: the ranged enemy. This can attack our player at range, and will do so if the player moves within range. We could implement this like so:

Suppose there’s another, seemingly unrelated requirement to implement a two-dimensional game board rather than the single row affair we’ve used so far:

Following the Open/Closed principle, we can extend our existing GameBoard class to be 2D, and simply track an additional axis of movement. At first glance this appears to do the trick, other than maybe the CurrentLocation variable now is oddly named.

Plug this new gameboard into our ranged enemy, however, and we might get unexpected behaviour. CurrentLocation is not enough to work out where the player is and whether he’s moved within range: what if he moves to the same row, but 6 columns away?

This is the sort of problem that might not be immediately obvious until further down the line when the mistake is more costly. The clue should have been the odd name of that property: if it is no longer descriptive, it no longer represents the same aspect as in the parent class, and that’s a classic indicator of a Liskov violation.

Post a comment