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:
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 isSquare
). - The x is
square
. Itâs an instance ofSquare
(S) which is subtype ofRectangle
(T) - The y is
rect
. Itâs an instance ofRectangle
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.setWidthf2 = Square.setHeightf3 = 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!