LSP: Liskov Substitution Principle
Enabling seamless class interchangeability and code reusability
Note: This publication demonstrates the Liskov Substitution Principle in the following languages: TypeScript and PHP.
LSP stands for Liskov Substitution Principle. It is one of the principles of Object-Oriented Design. Robert Cecil Martin¹ (widely known as Uncle Bob) introduces several principles in his famous book:
Agile Software Development, Principles, Patterns, and Practices
He described five OOD principles in this book. They are collectively known as the SOLID Principles. These principles have received wide attention in the software industry. The Liskov Substitution Principle is the third one of the five principles. Otherwise, L of SOLID.
This principle was written by Barbara Liskov in 1988. She gives a definition of the principle in her book²:
What is wanted here is something like the following substitution property : If for each object O1 of type S there is an object O2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when O1 is substituted for O2 then S is a subtype of T.
But later, Uncle Bob paraphrases this definition in his book as follows:
Subtypes must be substitutable for their base types.
What does the definition mean? Say, we have a function called factory(A)
. This function takes a reference of A
as an argument where A
is a class. B
is the child class of A
. As B
is a derivative of A
, we can pass in B
to the factory()
function as factory(B)
. If passing B
causes the factory()
function to misbehave, then B
violates LSP.
Here B
is a subtype of the base type A
. Therefore, the B
subtype must be substitutable for the base type A
. If not, that violates LSP. That is it.
Applying LSP
Let us consider a scenario where we require a download method for downloading products. Instead of including this method in each individual class, we can place it within an abstract class. By doing so, we avoid duplicating code.
// TypeScript
abstract class Product {
private filePath: string;
private response: Response;
constructor(filePath: string, response: Response) {
this.filePath = filePath;
this.response = response;
}
/**
* Downloads downloadable products
*
* @return {void}
*/
download(): void {
// Set headers for downloading file
this.response.setHeader('Content-Type', 'application/octet-stream');
this.response.setHeader(
'Content-Disposition',
`attachment; filename="${path.basename(this.filePath)}"`
);
// Read the file stream and pipe it to the response
// to be download as you said so in headers
const fileStream = fs.createReadStream(this.filePath);
fileStream.pipe(this.response);
}
}
// PHP
abstract class Product
{
private string $filePath;
public function __construct(string $filePath)
{
$this->filePath = $filePath;
}
/**
* Downloads downloadable products
*
* @return void
*/
public function download(): void {
// Set headers for downloading file
header('Content-Type', 'application/octet-stream');
header('Content-Disposition: attachment; filename="'
. basename($this->filePath) . '"');
// Read the file stream and output it to the response
readfile($this->filePath);
exit;
}
}
We are keeping the download()
method in the Product
abstract class to only explain LSP. Because the download functionality may be a different responsibility.
Now we need a client function that will take an object of a class that extends the Product
class as an argument.
// TypeScript
function download(product: Product): void {
product.download();
}
// PHP
function download(Product $product): void {
$product->download();
}
According to LSP, the download()
method should work if we pass any object that is created from the sub-classes of the Product
class. Because sub-classes should be substitutable by their base type. Otherwise, it will violate LSP. So let us create some sub-classes out of the Product
class.
// TypeScript
class Audio extends Product {}
class Video extends Product {}
// PHP
class Audio extends Product {}
class Video extends Product {}
It is time to create a couple of objects from these classes and pass them to the download()
function to check whether it can download them.
// TypeScript
app.get('/download', (req: Request, res: Response) => {
const audio = new Audio('path/to/audio.mp3', res);
const video = new Video('path/to/video.mp4', res);
// Passing in different objects
download(audio);
});
// PHP
$audio = new Audio('path/to/audio.mp3');
$video = new Video('path/to/video.mp4');
// Passing in different objects
download($audio);
download($video);
It should work. Because the Audio
and the Video
classes are sub-types of the base type the Product
class. So they should be substitutable. That makes the download()
function LSP compatible.
Now let us see what violates LSP. Look at the following code snippet:
// TypeScript
class Chocolate extends Product {
// Do other stuff
}
const chocolate = new Chocolate('What???', response);
// Passing in chocolate object
download(chocolate);
// PHP
class Chocolate extends Product
{
// Do other stuff
}
$chocolate = new Chocolate('What???');
// Passing in chocolate object
download($chocolate);
As we know Chocolate
is a product, so we could create a sub-class from the Product
class. Because they have an IS-A relation between them. So we pass the object to the download()
function. But the Chocolate
object has nothing to download. Therefore, the download()
method will not work.
What this means is we’ve violated the LSP. It is clear that nobody would pass the Chocolate
object to the download()
method. But as the object is from a sub-class of the Product
class, it is passable as an argument to the download()
method.
Big question!
Now the question is who is responsible for violating LSP? Is it the creator of the download()
function? Is it the creator of the Product
class? Or is it the creator of the Chocolate
class?
One might argue it is the creator of the download()
function. Because we know that all products are not downloadable. Nevertheless, the creator of that method uses Product
class as a parameter type.
But this is also true that the creator of the download()
function has every right to make the assumption that the download()
function only maintains downloadable products. There can have invariants of the Product
class that quite apply to the Product
class.
On the other hand, in general, the creator of the Chocolate
class did not violate the LSP. But it is who extends the Chocolate
class from the Product
class.
Notice, the download()
method’s Doc Block, it says this method will be used to download downloadable products. So you have to pass in an object which should be a sub-class of the Product
class and downloadable.
Why LSP?
LSP instructs how to properly inherit from classes. Therefore, LSP tells us how to use or create derived classes the way they should be.
“What are the design rules that govern this particular use of inheritance? What are the characteristics of the best inheritance hierarchies? What are the traps that will cause us to create hierarchies that do not conform to the OCP?” These are the questions addressed by the LSP.
So when would you know that the design of a class or module is appropriate? How many miles exactly should we go for that? Well, there are no perfect answers to these questions. But, it is such that one must view the design in terms of reasonable assumptions made by the users of that design.
Now another question comes up! Does anybody know what reasonable assumptions the users of that design are going to make? Those assumptions are not easy to anticipate. If you put all the assumptions with the design, that will be the smell of Needless Complexity. This is not good to go. We do not want to load the design with lots of unnecessary abstractions. So what to do?
There is a way to provide some reasonable assumptions by following the design principle called Design by Contract.
Design by Contract
We should not put all reasonable assumptions in our code. But there is a technique to make some reasonable assumptions explicit. The technique is called design by contract and was coined by Bertrand Meyer³.
Using the DBC, an author of a class dictates the contract for that class. The contract may be specified by declaring pre-conditions and post-conditions for each method of a class. The pre-conditions and the post-conditions are language features. Languages like TypeScript, JavaScript, PHP, Java, etc. do not support them. But we may apply pre-conditions and post-conditions using Doc Block comments.
We did so with the download()
method in the Product
class. The Doc Block says the method can only download downloadable products and its return tag is set to void
.
Now, for some reason, if the author needs different download logic, then he/she may implement the brand new logic in the sub-classes. If the author implements more or less than what the download()
method’s Doc Block says to do, therefore, if the download()
function does not work anymore with the new implementation of the download()
method of a sub-class, that will violate the LSP.
Note that the pre-conditions and post-conditions are very simple in our case. It could be different in other cases. Say, the download()
method could have been parameters, return types, and other conditions or logic.
Do not put all reasonable assumptions in your design but try to take care of common LSP violations.
Common LSP violations
Some simple heuristics may give us some clues about LSP violations. They are all about derivative classes.
Degenerate Methods in Subclasses — If a base class has a method, but a subclass from the base class does not need that method, then if the author of the subclass degenerates the method again, it will be a substitutable violation.
// TypeScript
class ParentClass {
test() {
console.log(this.test.name);
}
}
class ChildClass extends ParentClass {
// Notice no code is in test() method
test() {}
}
class ParentClass
{
public function test()
{
var_dump(__METHOD__);
}
}
class ChildClass extends ParentClass
{
// Notice no code is in test() method
public function test() {}
}
Throwing Exceptions from Subclasses - Another form of LSP violation is the addition of an exception to the subclass while the base class does not expect that. Because then the base class can not be substituted by the subclass.
Summary
By adhering to LSP, we can create more flexible and reusable code. The Liskov Substitution Principle (LSP) tells us that any instance of a base class should be replaceable with an instance of its derived class without affecting the correctness of the program: the download()
function.
The principle emphasizes the importance of maintaining behavioral compatibility between base and derived classes. It promotes designing classes in a way that ensures derived classes adhere to the same contract and behavior as their base classes. This enables class interchangeability and code reusability seamlessly.
See full code here: TypeScript and PHP.
Thanks for reading
- 👏 Please clap for this publication
- 🔔 Follow me: Medium | LinkedIn | Twitter
- 🫵 Read more on writing Efficient, Reusable, and Maintainable code