Understanding SOLID Principles: Liskov Substitution Principle
Tamerlan Gudabayev
Posted on February 7, 2021
insert eye-catching line
get your attention to read the post further down
Okay now that I got your attention, today we are going to talk about one of the SOLID principles.
Primarily the L in SOLID stands for Liskov Substitution Principle.
Unlike other principles, this one actually has some set of rules to follow, but don't worry it's a lot simpler than you think.
PS. From now on I'm gonna be referring to the Liskov Substitution Principle as LSP.
Table of Contents
- What is the LSP?
- Why is the LSP important?
- LSP Rules
- How to identify LSP violations?
- Example
- Conclusion
- Additional Resources
What is the LSP?
As programmers our first instinct is to google shit, if we google "LSP or Liskov..." we will get something like this.
LSP is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, a task performed, etc.).
Yea, I too didn't understand shit.
But on the bright side it's actually a simple concept, let me explain.
PS. I know it's super cliche to explain the LSP using shapes, but it's as simple as it gets. Don't worry in the example section we will tackle a real-world problem.
Let's say you were tasked to create a program that manages rectangles and returns their area.
Your like "easy", so you go on and create a rectangle class.
<?php
// Imports and namespace...
class Rectangle {
$public width;
$public height;
public function getArea(){
return $this->width * $this->height;
}
public function setWidth(width){
$this->width = width;
}
public function setHeight(height){
$this->height = height;
}
}
All nice and dandy, then one day your project manager comes and says "we need to implement squares now"
You say "it's easy" but at the same time you remember the DRY (Don't Repeat Yourself) principle, and your just lazy to rewrite code. So you conclude that a rectangle and a square are essentially the same things but the only difference is that squares have equal width and height.
So you come up with something like this.
<?php
// Imports and namespace...
class Square extends Rectangle {
public function setWidth(width){
$this->width = width;
$this->height = width;
}
public function setHeight(height){
$this->height = height;
$this->width = height;
}
}
Our main class would look something like this:
<?php
$shape = new Rectangle();
$shape->setWidth(5);
$shape->setHeight(3);
$shape->getArea(); // This will return 15
Now let us substitute the rectangle object with a square one.
<?php
$shape = new Square();
$shape->setWidth(5);
$shape->setHeight(3);
$shape->getArea(); // This will return 9
To another developer, this might seem very weird because the $shape→setWidth(5)
method doesn't do anything. After all, the $shape→setHeight(3)
method overrides both the width and the height. This is unexpected behavior. Our function is called setHeight
but it also changes the width of the shape??
This is a clear violation of the LSP because the client now is confused because both setWidth
and setHeight
methods do stuff it's not meant to do, which might break the client.
Why is the LSP important?
The main issue is that LSP violations cause code smell.
What is code smell? You may ask.
As Martin Fowler said:
A code smell is a surface indication that usually corresponds to a deeper problem in the system.
If you generalize a concept too much, you may create a superclass where none is needed. In layman's terms if you can't substitute a superclass by its subclasses then you will be forced to use a whole bunch of if-else statements or switch statements, to specially handle subclasses.
LSP allows code to work more like hardware.
You don't have to modify your iPod because you bought a new pair of external speakers since both the old and the new external speakers respect the same interface, they are interchangeable without the iPod losing desired functionality.
LSP Rules
LSP is the only principle in solid that has a set of rules, most of them are not very practical to break, but some of them may be tricky. Let's go through them all one by one.
-
Parameter types in a method of a class should match or are more abstract than parameter types in the superclass. Sounds confusing? Let's have an example.
- Imagine there's a class with a method that's supposed to feed dogs:
feed(Dog d)
. Client code always passes dog objects into this method. -
Good: Say you created a subclass that overrode the method so that it can feed any animal (a superclass of dogs):
feed(Animal d)
. Now if you pass an object of this subclass instead of an object of a superclass to the client code, everything would still work fine. The method can feed all animals, so it can still feed any cat passed by the client. -
Bad: You created another class and restricted the feeding method to only accept bulldogs (a subclass of dogs):
feed(Bulldog d)
. If you pass an object of this subclass instead of an object of the superclass to the client code, then it would break functionality because you can only feed a specific breed of dogs.
- Imagine there's a class with a method that's supposed to feed dogs:
-
The return type in a method of a subclass should match or be a subtype of the return type in the superclass. The requirements for the return type is inverse to the requirements of the parameter type.
- Imagine this, you got a class with a method
buyDog(): Dog
. The client code expects to receive a dog as a result of executing this method. -
Good: A subclass overrides the method as follows:
buyCat(): Bulldog
. The client gets a bulldog which is still a dog, so everything is okay. -
Bad: A subclass overrides the method as follows:
buyCat(): Animal
. Now the client code breaks since it receives an unknown generic animal (a cat? a lion?) that doesn't fit the structure designed for a dog.
- Imagine this, you got a class with a method
-
A method in a subclass shouldn't throw types of exceptions that the base method isn't expected to throw. Meaning that the types of exceptions should match or be subtypes of the ones that the base method is already able to throw.
- This is a little bit impractical because in most statically typed languages (Java, C#, and others), these rules are built into the language. You won't be able to compile a program that violates these rules.
-
A subclass shouldn't strengthen pre-conditions. For example, you have a method that accepts an
int
. If a subclass overrides this method and requires theint
to be positive (by throwing an exception if it is negative), this strengthens the pre-conditions. The client code which used to work fine by passing in negative numbers into the method now breaks if it starts working with an object of this subclass. - A subclass shouldn't weaken post-conditions. Post-conditions describe the state of objects after a process is completed. For example, it may be assumed that the database connection is closed after executing a SQL statement. You might create a subclass and changed it so that the database connections remain open so you can reuse them. But the client might not know that, because it expects the method to close all connections, it may simply terminate the program right after calling the method, polluting the system with ghost database connections.
-
Invariants of a superclass must be preserved. A class invariant is an assertion concerning object properties that must be true for all valid states of the object. Basically if the parent class has a variable that must be lower than 0 then all subclasses must follow this rule or else the client code will have unexpected behavior.
- This is the least formal rule of all. This rule might be the easiest to violate because you might misunderstand or not realize all of the invariants of a complex class. Therefore, the safest way to extend a class is to introduce new fields and methods, and not mess with any existing members of the superclass. Of course, that’s not always doable in real life.
The LSP is applicable when there’s a supertype-subtype inheritance relationship by either extending a class or implementing an interface. We can think of the methods defined in the supertype as defining a contract. Every subtype is expected to stick to this contract. If a subclass does not adhere to the superclass’s contract, it’s violating the LSP.
This makes sense intuitively - a class’s contract tells its clients what to expect. If a subclass extends or overrides the behavior of the superclass in unintended ways, it would break the clients.
How to identify LSP violations?
Some good indicators to identify LSP violations are:
- Conditional logic (using the
instanceof
operator orobject.getClass().getName()
to identify the actual subclass) in client code - Empty, do-nothing implementations of one or more methods in subclasses
- Throwing an
UnsupportedOperationException
or some other unexpected exception from a subclass method
For point 3 above, the exception needs to be unexpected from the superclass’s contract perspective. So, if our superclass method’s signature explicitly specified that subclasses or implementations could throw an UnsupportedOperationException
, then we would not consider it as an LSP violation.
Example
Let's look at an example of document classes that violate the substitution principle.
<?php
class Document {
public $data;
public $filename;
public function open(){
// Logic for opening the document
}
public function save(){
// Logic for saving the document
}
}
<?php
class ReadOnlyDocument extends Document
{
public function save()
{
throw new Exception("Can't save read-only document");
}
}
<?php
/** @var $documents Document[] */
class Project
{
public $documents;
public function openAll(){
foreach ($this->documents as $doc){
$doc->open();
}
}
public function saveAll(){
foreach ($this->documents as $doc){
if(!is_a($doc, ReadOnlyDocument::class)){
$doc->save();
}
}
}
}
The save
method in the ReadOnlyDocument
subclass throws an exception if someone tries to call it. The base method doesn't have this restriction, meaning that the client code will break if we don't check the document type before saving it.
The resulting code also breaks the open/closed principle, because now the client code is dependent on concrete classes of documents. If we introduce a new document subclass, we will have to change the client code to support it.
We can solve this problem by redesigning the class hierarchy: the save
method is our anomaly here, so we simply take it out and make the ReadOnlyDocument
the base class, with WritableDocument
a subclass that adds the behavior of saving.
<?php
class Document {
public $data;
public $filename;
public function open(){
// Logic for opening the document
}
}
<?php
class WritableDocument extends Document
{
public function save()
{
// Logic for saving the document
}
}
<?php
/** @var $allDocs Document[] */
/** @var $writableDocs WritableDocument[] */
class Project
{
public $allDocs;
public $writableDocs;
public function openAll(){
foreach ($this->allDocs as $doc){
$doc->open();
}
}
public function saveAll(){
foreach ($this->writableDocs as $doc){
$doc->save();
}
}
}
Conclusion
In summary:
- LSP is one of the SOLID principles which states that subclasses should be able to substitute their parent classes without breaking any client functionality.
- Violations of LSP cause code smell, so try to stay away from them.
- LSP has a set of rules to follow, some of which are not practical but others that can be tricky.
I hope you learned something today, feel free to ask questions in the comments section below.
Additional References
https://refactoring.guru/design-patterns
http://www.blackwasp.co.uk/lsp.aspx
https://stackoverflow.com/questions/56860/what-is-an-example-of-the-liskov-substitution-principle
Posted on February 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.