Barbara Liskov Substitution Principle (LSP): How you are getting it wrong and how to get it right

23 Feb, 2023
1963 words | 10 min to read | 5 hr, 53 min to write

LSP. Barbara Liskov Substitution Principle.

The most misunderstood, yet the most strictly defined principles of SOLID.

How did that happen? 🤨

THE definition

Wikipedia provides the original definition of LSP:

Subtype Requirement: Let f(x) be a property provable about objects x of type T. Then f(y) should be true for objects y of type S where S is a subtype of T.

And at this points most people be like

So, this definition needs a little “explain me like I’m 5” rephrasing. And, there we have it, all types of folks coming up with all types of definitions, which are easier to read:

Simply put, the Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

source: blog.knoldus.com

At a high level, the LSP states that in an object-oriented program, if we substitute a superclass object reference with an object of any of its subclasses, the program should not break.

source: reflectoring.io

And a better one:

derived class objects must be substitutable for the base class objects. That means objects of the derived class must behave in a manner consistent with the promises made in the base class’ contract.

source: wiki.c2.com

And guess what? They are all correct, I won’t even try rephrasing, because it’s most likely that I will end up with something similar.

What’s incorrect is assumptions that may be made of that statements. Just like they say “The devil is in the details”, LSP is all about semantics.

So, I can replace my Rectangle with a Square?

Let’s try something like this

class Rectangle {
public function __construct(
protected int $w,
protected int $h
) { }
public function area(): int {
return $this->w * $this->h;
}
}
class Square extends Rectangle {
public function __construct(int $side) {
parent::__construct($side, $side)
}
}

Let’s now create a rectangle and calculate it’s area.

$rect = new Rectangle(4, 5);
$rect->area(); // 20

And now some people will assume that LSP suggests we can substitute types like this:

- $rect = new Rectangle(4, 5);
+ $rect = new Square(4, 5);
$rect->area(); // 20

and the program should work.

This is WRONG.

Firstly, the program will not work because of the constructor difference. Constructor is not even part of the type, it’s an initialisation method.

What LSP never mentions is that types are substitutable. It basically says if superclass methods and public properties reference subclass methods and public properties (hence, substituting them), the program should run correctly.

A valid case of LSP violation using Rectangle and Square example.

There is a What is an example of the Liskov Substitution Principle? stackoverflow question which made me write this whole article.

The accepted answer is this what seems to be a canonical example of LSP violation.

Let’s recreate the example from text. And, let’s add an interface too.

interface RectangleLikeInterface {
public function setWidth(int $w): void;
public function setHeight(int $w): void;
public function area(): int;
}
class Rectangle implements RectangleLikeInterface {
protected int $w;
protected int $h;
public function setWidth(int $w) {
$this->w = $w;
}
public function setHeight(int $h) {
$this->h = $h;
}
public function area(): int {
return $this->w * $this->h;
}
}
class Square extends Rectangle {
public function setWidth(int $w) {
$this->w = $this->h = $w; // since w == h in a square
}
public function setHeight(int $h) {
$this->w = $this->h = $w; // since w == h in a square
}
}

Let’s, once again, use the rectangle.

$rect = new Rectangle;
$rect->setWidth(5);
$rect->setHeight(4);
$rect->area(); // 20

Now, let’s do a mental exercise and replace $rect->setWidth(5), $rect->setHeight(4), and $rect->area() with the business logic of Square::setWidth, Square::setHeight, and Square::area.

Square::area is unchanged, so it doesn’t break LSP. What does though is Square::setWidth and Sqaure::setHeight, because they change behaviour.

The behaviour is driven by business logic and state of an object. State is protected int $w and proctected int $h. The Sqaure implementation changes behaviour of setWidth and setHeight to set both protected int $w and proctected int $h to the same value.

And hence, we have the violation. And this is exactly what people mean when they say that LSP simply prescribes that the behaviour of the derived classes must not be altered.

If a subclass adds behaviour on top, no issues with the LSP. I.e. if I derive a ColouredRectangle from a Rectangle, there is no LSP violation:

class ColouredRectangle extends Rectangle {
public function __construct(protected Colour $colour) {}
public function getColour(): Colour {
return $this->colour;
}
}

We did change the constructor, but as I said before, constructor isn’t part of the type, it’s merely a way to initialise object upon instantiation, to set initial state.

A better example

In the same stackoverflow post, there is a better example of LSP violation. At least I like it more.

It also gives an even better “explain me like I’m 5” definition (well, 5 is unlikely, but I wage a cup of coffee a programmer with some 3-4 years of experience can understand it):

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

source: stackoverflow.com

And here’s the better example:

It’s less subtle though. Clearly, we are redefining the interface, method that used to accept 2 arguments, now accept 3. The return value has changed in some of the methods.

So, if I use interfaces, I never violate LSP?

Nope.

The chance that you, or any other programmer in your organisation, violates LSP when you have well defined interfaces is less than when you don’t have well-defined interfaces (or interfaces in a first place).

Interfaces are only “external” (client-facing) contract. LSP, on the other hand, is all about what code does internally, and can not be enforced.

I still strongly advice to use interfaces. They can lesser the chance of violating LSP because they give less room for messing the code internally provided that they oblige a method to take in exactly the parameters defined on the interface, and return a value which is also part of the interface.

Also remember, that not all the languages have strict interfaces such as Java or C++. There are languages with duck typing such as go or python. There are languages where interfaces are emulated such as ruby, and again python.

If it quacks like a duck…

Another thing I wanted to point out to is this meme people merrily copy-pasting all across the internet, which is a funny allusion on LSP

Well, this is WRONG.

I’m sorry, okay? I kinda like it, it’s funny… But, yeah, it’s actually not funny if you think about it for some time.

Thing is… If it looks like a duck, quacks like a duck, but needs batteries… WHY DO I CARE IF IT NEEDS BATTERIES?

…

No? Didn’t get it?

Okay…

Let’s say, I have KindOfDuck interface which implements both LooksLikeADuck, and QuacksLikeADuck:

interface LooksLikeADuck {
public function looksLike(): DuckImage;
}
interface QuacksLikeADuck {
public function quack(): string;
}
interface KindOfDuck implements LooksLikeADuck, QuacksLikeADuck {
}

And now, I’m consuming it:

class Pond {
public function addDuck(KindOfDuck $duck) {
// ...
}
}

So, why do I care about batteries, again?

If my contract requires something to only look like a duck and quack like a duck, I don’t even want to know if it works off of batteries, I just need it to quack…

Let’s revisit the original definition

… and stop being scared of it.

I will use python this time as it’s better suited for the purpose (you will understand why in the coming examples).

Let’s rewrite both implmenetations of Rectangle and Square. The one that does violate the LSP, and the one that does not. And then we apply the original definition to both implementations.

Python version of Rectangle and Square that does not violate LSP

class Rectangle:
def __init__(self, w, h) -> None:
self.w = w
self.h = h
def area(self) -> int:
return self.w * self.h
class Square(Rectangle):
def __init__(self, size) -> None:
super().__init__(size, size)

Okay, let’s bring back the definiton:

Let f(x) be a property provable about objects x of type T.

Then f(y) should be true for objects y of type S

where S is a subtype of T.

And let’s now validate it for the area in the following code:

rect = Rectangle(4, 5)
rect.area()
square = Square(5)
square.area()

Now, please understand this… You only need to understand this ONCE to be enlightened.

  • The S is Square
  • The T is Rectangle
  • The f is area (the method defined on type S which is Square).
  • The x is square. It’s an instance of Square (S) which is subtype of Rectangle (T)
  • The y is rect. It’s an instance of Rectangle

Hence, we have:

rect = Rectangle(4, 5) # y = T(4, 5)
rect.area() # f(y)
square = Square(5) # x = S(5)
square.area() # f(x)

Let’s do the substitution on the T (Rectangle). And this is where one property of python comes handy:

f = Square.area # since in python all methods are first-class objects
# now we can run business logic defined in Square.area
# on an instance (y) of Rectangle (T)
y = Rectangle(4, 5)
f(y) # still 20, so LSP is not violated

Let’s do the same trick with the Rectangle and Square examples that does violate LSP

Python version of Rectangle and Square that DOES violate LSP

class Rectangle:
def setWidth(self, w) -> None:
self.w = w
def setHeight(self, h) -> None:
self.h = h
def area(self) -> int:
return self.w * self.h
class Square(Rectangle):
def setWidth(self, w) -> None:
self.w = w
self.h = w
def setHeight(self, h) -> None:
self.w = h
self.h = h
rect = Rectangle()
rect.setWidth(5)
rect.setHeight(4)
print(rect.area()) # 20, which is correct
# we don't need to create Square since substitution happens on the instance of Rectangle (T)

Let’s rewrite a little so align with the original definition of LSP better and borrow setWidth and setHeight from Sqaure (S);

f1 = Square.setWidth
f2 = Square.setHeight
f3 = Square.area
rect = Rectangle()
f1(rect, 5)
f2(rect, 4)
print(f3(rect)) # 16 !

Check out this example yourself if you don’t believe me ;)

I hope now it’s super-clear how LSP is actually used.

When, how, and why is it important to use LSP?

Any time you have inheritance in your code. Especially if you override methods through inheritance.

Remember: inheritance is POWERFUL and DANGEROUS technique in OOP languages. With great power comes great responsibility, they say. And this is for the reason.

If inheritence is misused, you risk ending up with not only spaghetti code, but with LAYERS of spaghetti code. A lasagna code, if you may.

LSP for OOP is somewhat what pure functions are for functional programming. Not exactly, but somewhat. Both concepts are achieving the same thing in our programs - minimising side effects.

Why is LSP violations are bad? Well, have a look at the examples of Rectangle and Square, and Board and ThreeDBoard. Now, extrapolate it. Let’s say you have > 1 level of inheritance. It may get out of hand very quick if you don’t adhere to LSP!