Liskov Substitution Principle

Derived classes must be substitutable for their base classes.

The substitution principle applies to well-designed class inheritance. The author of this principle is Barbara Liskov. The principle says that we can use any inheriting class in place of the base class. If we implement a subclass, we must also be able to use it instead of the main class. Otherwise, it means that inheritance has been implemented incorrectly.

There are some popular examples of the Liskov substitution principle in PHP:

Examples

rectangle-square

The first example. We already have a Rectangle PHP class. Now we're adding a Square PHP class that inherits the Rectangle class. Because every square is also a rectangle :). They have the same properties, height and width.

The height of the square is the same as the width. So, setHeight() and setWidth() will set both (what about single responsibility?) of these values:

PHP
class Square extends Rectangle
{
    public function setWidth(int $width): void { 
        $this->width = $width;
        $this->height = $width;
    }
 
    public function setHeight(int $height): void {
        $this->width = $height;
        $this->height = $height;
    }
}

Is that a good solution? Unfortunately, it does not follow the Liskov substitution principle. Let's say there is a test that computes the area of a rectangle, and it looks like this:

PHP
public function testCalculateArea()
{
    $shape = new Rectangle();
    $shape->setWidth(10);
    $shape->setHeight(2);
 
    $this->assertEquals($shape->calculateArea(), 20);
 
    $shape->setWidth(5);
    $this->assertEquals($shape->calculateArea(), 10);
}

According to the Liskov substitution principle, we should be able to replace the Rectangle class with the Square class. But if we replace it, it turns out that the test does not pass (100 != 20). Overriding the setWidth() and setHight() methods broke the Liskov substitution rule. We should not change how the parent class's methods work.

So what is the correct solution? Not every idea from "reality" should be implemented 1:1 in code. The Square class should not inherit from the Rectangle class. If both of these classes can have a computed area, let them implement a common interface, and not inherit one from the other since they are quite different.

You can see an example solution here

live duck vs toy duck

Imagine a living duck and a toy duck and their representations in the code (PHP classes). Both of these classes implement the TheDuck interface.

PHP
interface TheDuck
{
    public function swim(): void;
}

We also have a controller with the action swim().

PHP
class SomeController
{
    public function swim(): void
    {
        $this->releaseDucks([
            new LiveDuck(),
            new ToyDuck()
        ]);
    }
 
    private function releaseDucks(array $ducks): void
    {
        /** @var TheDuck $duck */
        foreach ($ducks as $duck) {
            $duck->swim();
        }
    }
}

But after calling this action ToyDuck doesn't swim. Why? Because to make it swim, you must first call the "turnOn()" method.

PHP
class ToyDuck implements TheDuck
{
    private bool $isTurnedOn = false;
 
    public function swim(): void 
    {
        if (!$this->isTurnedOn) {
            return;
        }
 
        // ...
    }
}

We could modify the controller action and add a condition that we call turnOn() on the ToyDuck instance before swim().

PHP
private function releaseDucks(array $ducks): void
{
    /** @var TheDuck $duck */
    foreach ($ducks as $duck) {
        if ($duck instanceof ToyDuck) {
            $duck->turnOn();
        }
            
        $duck->swim();
    }
}

It violates the Liskov substitution principle because we should be able to use a subclass without knowing the object, so we cannot condition by subclasses (it also violates the open/close principle - because we need to change the implementation).

Handling a collection of objects of a given base class may not require checking whether the given object is an instance of subclass X and should be treated differently.

What should it look like correctly? A common interface for both of these ducks is not a good idea, their operation is completely different, even though we think they both work similarly because they are swimming, it is not.

ReadOnlyFile

And the last example. We have a File class with methods read() and write().

PHP
class File
{
    public function read()
    {
       // ...
    }
 
    public function write()
    {
       // ...
    }
}

We're adding a new class - ReadOnlyFile.

PHP
class ReadOnlyFile extends File
{
    public function write()
    {
        throw new ItsReadOnlyFileException();
    }
}

The ReadOnlyFile class inherits from the File class. In the ReadOnlyFile class, the write() method will throw an Exception, because you cannot write to a read-only file.

This is a poorly designed abstraction, the Liskov rule has been broken because we are unable to use the ReadOnlyFile class instead of File.

Conclusion

Following the Liskov Substitution Principle is a good indicator that you are following a correctly hierarchy schema. And if you don’t follow it, the unit tests for the superclass would never succeed for the subclasses.