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(); // 20And 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(); // 20and 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(); // 20Now, 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 violatedLetā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 = hrect = 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!