What is an example of the Liskov Substitution Principle?
A great example illustrating LSP (given by Uncle Bob in a podcast I heard recently) was how sometimes something that sounds right in natural language doesn't quite work in code.
In mathematics, a Square
is a Rectangle
. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square
derive from Rectangle
, then a Square
should be usable anywhere you expect a Rectangle
. This makes for some strange behavior.
Imagine you had SetWidth
and SetHeight
methods on your Rectangle
base class; this seems perfectly logical. However if your Rectangle
reference pointed to a Square
, then SetWidth
and SetHeight
doesn't make sense because setting one would change the other to match it. In this case Square
fails the Liskov Substitution Test with Rectangle
and the abstraction of having Square
inherit from Rectangle
is a bad one.
Y'all should check out the other priceless SOLID Principles Explained With Motivational Posters.
The Liskov Substitution Principle (LSP, lsp) is a concept in Object Oriented Programming that states:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
At its heart LSP is about interfaces and contracts as well as how to decide when to extend a class vs. use another strategy such as composition to achieve your goal.
The most effective way I have seen to illustrate this point was in Head First OOA&D. They present a scenario where you are a developer on a project to build a framework for strategy games.
They present a class that represents a board that looks like this:
All of the methods take X and Y coordinates as parameters to locate the tile position in the two-dimensional array of Tiles
. This will allow a game developer to manage units in the board during the course of the game.
The book goes on to change the requirements to say that the game frame work must also support 3D game boards to accommodate games that have flight. So a ThreeDBoard
class is introduced that extends Board
.
At first glance this seems like a good decision. Board
provides both the Height
and Width
properties and ThreeDBoard
provides the Z axis.
Where it breaks down is when you look at all the other members inherited from Board
. The methods for AddUnit
, GetTile
, GetUnits
and so on, all take both X and Y parameters in the Board
class but the ThreeDBoard
needs a Z parameter as well.
So you must implement those methods again with a Z parameter. The Z parameter has no context to the Board
class and the inherited methods from the Board
class lose their meaning. A unit of code attempting to use the ThreeDBoard
class as its base class Board
would be very out of luck.
Maybe we should find another approach. Instead of extending Board
, ThreeDBoard
should be composed of Board
objects. One Board
object per unit of the Z axis.
This allows us to use good object oriented principles like encapsulation and reuse and doesn’t violate LSP.