The Liskov Substitution Principle

Share this article

Welcome to the (overridden) Matrix. Hush… don’t tell anybody! In a deleted scene from the Matrix trilogy, the following dialogue takes place: Morpheus: Neo, I’m inside the Matrix right now. Sorry to give you the bad news but our agent-tracking PHP program needs a quick update. It currently uses PDO’s query() method with strings to fetch the status of all the Matrix agents from our database, but we need to do that with prepared queries instead. Neo: Sounds fine, Morpheus. Can I get a copy of the program? Morpheus: No problem. Just clone our repository and take a look at the AgentMapper.php and index.php files. Neo issues a few Git commands and soon the following code appear before his eyes.

<?php
namespace ModelMapper;

class AgentMapper
{
    protected $_adapter;
    protected $_table = "agents";

    public function __construct(PDO $adapter) {
        $this->_adapter = $adapter;
    }

    public function findAll() {
        try {
            return $this->_adapter->query("SELECT * FROM " . $this->_table, PDO::FETCH_OBJ);
        }
        catch (Exception $e) {
            return array();
        }
    }   
}
<?php
use ModelMapperAgentMapper;

// a PSR-0 compliant class loader
require_once __DIR__ . "/Autoloader.php";

$autoloader = new Autoloader();
$autoloader->register();

$adapter = new PDO("mysql:dbname=Nebuchadnezzar", "morpheus", "aa26d7c557296a4e8d49b42c8615233a3443036d");

$agentMapper = new AgentMapper($adapter);
$agents = $agentMapper->findAll();

foreach ($agents as $agent) {
    echo "Name: " . $agent->name .  " - Status: " . $agent->status . "<br>";
}
Neo: Morpheus, I just got the files. I’m going to subclass PDO and override its query() method so it can work with prepared queries. Because of my superhuman powers, I should be able to get this working in a snap. Keep calm. The smooth sound of a computer keyboard fills the air. Neo: Morpheus, the subclass is ready to be tested. Feel free to check it out on your side. Morpheus does a quick search on his laptop and sees the class below.
<?php
namespace LibraryDatabase;

class PdoAdapter extends PDO
{
    protected $_statement;

    public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) {
        // check if a valid DSN has been passed in
        if (!is_string($dsn) || empty($dsn)) {
            throw new InvalidArgumentException("The DSN must be a non-empty string.");
        }
        try {
            // attempt to create a valid PDO object and set some attributes.
            parent::__construct($dsn, $username, $password, $driverOptions);
            $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }

    public function query($sql, array $parameters = array())
    {
        try {
           $this->_statement = $this->prepare($sql);
           $this->_statement->execute($parameters);
           return $this->_statement->fetchAll(PDO::FETCH_OBJ);        
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }
}
Morpheus: The adapter looks good. I’ll give it a shot right away just to check if our agent mapper will be able to keep track of the active agents traveling across the Matrix. Wish me luck. Morpheus hesitates for a moment and runs the previous index.php file, this time using Neo’s masterpiece PdoAdapter class. And then, a scream! Morpheus: Neo, I’m sure you’re the One! It’s just that I got an awful fatal error on my face with the following message:
Catchable fatal error: Argument 2 passed to LibraryDatabasePdoAdapter::query() must be an array, integer given, called in path/to/AgentMapper on line (who cares?)
Another scream. Neo: What went wrong?! What went wrong?! More screams. Morpheus: I really don’t know. Oh, Agent Smith is now coming for me! The communication suddenly goes off. A long, heavy silence wraps up the dialogue, suggesting that Morpheus got caught by surprise and was seriously injured by Agent Smith.

LSP Doesn’t Stand for (L)azy, (S)illy (P)rogrammers

Needless to say the dialog above is fictional, but the problem is unquestionably real. If Neo had learned only one or two things about the Liskov Substitution Principle (LSP) as the renowned hacker he used to be, Mr. Smith could have been traced in a jiffy. Best of all, Morpheus would have been saved from the agent’s evils intentions. What a pity for him, indeed. In many cases, however, PHP developers think about the LSP pretty much as Neo did before: LSP is nothing but a purist’s theoretical principle that has little or no application in practice. But they’re treading down the wrong path. Even when the formal definition of the LSP makes eyes roll back (including mine), at its core it boils down to avoiding brittlely-defined class hierarchies where the descendants expose a behavior radically different from the base abstractions consuming the same contract. In simple terms, the LSP establishes that when overriding a method in a subclass, it must fulfill the following requirements:
  1. Its signature must match that of its parent
  2. Its preconditions (what to accept) must be the same or weaker
  3. Its post conditions (what to expect) must be the same or stronger
  4. Exceptions (if any) must be of the same type than the ones thrown by its parent
Now, feel free to reread the above list over again (don’t worry, I’ll wait), and you’ll hopefully realize why this makes a lot of sense. Back to the example, Neo’s cardinal sin was simply not to keep method signatures the same, hence breaking the contract with client code. To fix up this issue, the agent mapper’s findAll() method could be rewritten with some conditionals (a clear sign of code smell), as shown below:
<?php
public function findAll() {
    try {
        return ($this->_adapter instanceof PdoAdapter)
            ? $this->_adapter->query("SELECT * FROM " . $this->_table)
            : $this->_adapter->query("SELECT * FROM " . $this->_table, PDO::FETCH_OBJ);
    }
    catch (Exception $e) {
        return array();
    }
}
If you’re in a good mood and give the refactored method a try, it will work just fine, either when using a native PDO object or an instance of the PDO adapter. It may sound rough, I know, but this is only a quick and dirty fix which flagrantly violates the Opened/Closed principle. On the other hand, it’s feasible to refactor the adapter’s query() method in order to match the signature of its overridden parent. But in doing so, all of the other conditions stated by the LSP should be fulfilled also. Simply put, this means that method overriding should be done with due caution, and only with strong, really strong, reasons. In many use cases, and assuming that it’s not possible to use Interfaces
, it’s preferable to create subclasses that only extend (not override) the functionality of their base classes. In the case of Neo’s PDO adapter, this approach will function like a charm, and definitively won’t blow up client code at any level. As I just said, there’s a more efficient – yet radical – solution which appeals to the goodies of implementing interfaces. While the earlier PDO adapter was created via inheritance and admittedly broke the LSP commandments, the flaw comes actually from the way the agent mapper class was designed in the first place. Effectively, it relies from top to bottom on a concrete database adapter implementation, rather than on the contract defined by an interface. And the big OO powers say from ancient times that this is always a bad thing. So, how would the aforementioned solution be brought to life?

Design by Contract and the Case against Inheritance

Well, first off it’d be necessary to define a simple contract, which should be implemented later on by concrete database adapters. A trivial interface, like the one below, will do the trick nicely:
<?php
namespace LibraryDatabase;

interface DatabaseAdapterInterface
{
    public function connect();    
    public function disconnect();
    public function query($sql);
}
So far, so good. With this interface up and running already, spawning a concrete database adapter is as simple as creating the following implementer:
<?php
namespace LibraryDatabase;

class PdoAdapter implements DatabaseAdapterInterface
{
    protected $_config = array();
    protected $_connection;  

    public function __construct($dsn, $username = null, $password = null, array $driverOptions = array())
    {
        if (!is_string($dsn) || empty($dsn)) {
            throw new InvalidArgumentException("The DSN must be a non-empty string.");
        }
        // save connection parameters in the $_config field
        $this->_config = compact("dsn", "username", "password", "driverOptions");
    }

    public function connect()
    {
        // if there is a PDO object already, return early
        if ($this->_connection) {
            return;
        }
        // otherwise try to create a PDO object
        try {
            $this->_connection = new PDO(
                $this->_config["dsn"], 
                $this->_config["username"], 
                $this->_config["password"], 
                $this->_config["driverOptions"]);
            $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            $this->_connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
            
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }
    
    public function disconnect() {
        $this->_connection = null;
    }
    
    public function query($sql, $fetchStyle = PDO::FETCH_OBJ) {
        $this->connect();
        try {
            return $this->_connection->query($sql, $fetchStyle);
            
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }
}
Done. Even when the above class is pretty contrived, it adheres neatly to the contract that it implements. This allows for a bunch of SQL queries to be run without much struggle, and above all, without overriding anything relevant by mistake. The last change that must be made is to define an additional segregated interface, and refactor the initial mapper class so that the pitiless agents in the Matrix can be easily traced and beaten down in turn by injecting different database adapters at runtime. That said, here’s how the aforementioned interface looks:
<?php
namespace ModelMapper;

interface AgentMapperInterface {
    public function findAll();
}
And here’s the revamped version of the agent mapper:
<?php
namespace ModelMapper;
use LibraryDataBaseDatabaseAdapterInterface;

class AgentMapper implements AgentMapperInterface
{
    protected $_adapter;
    protected $_table = "agents"; 

    public function __construct(DatabaseAdapterInterface $adapter) {
        $this->_adapter = $adapter;
    }
    
    public function findAll() {
        try {
            return $this->_adapter->query("SELECT * FROM " . $this->_table);
        }
        catch (Exception $e) {
            return array();
        }
    }      
}
Mission accomplished. Considering that now the refactored mapper expects to receive an implementer of the earlier database interface, it’s safe to say that its findAll() method won’t need to go into ugly checks to see what it got injected in the constructor. Definitively, we have a big winner here! What’s more, the following code snippet shows how to put the previous elements to work side by side in sweet harmony:
<?php
use LibraryDatabasePdoAdapter,
    ModelMapperAgentMapper;

// a PSR-0 compliant class loader
require_once __DIR__ . "/Autoloader.php";

$autoloader = new Autoloader();
$autoloader->register();

$adapter = new PdoAdapter("mysql:dbname=Nebuchadnezzar", "morpheus", "aa26d7c557296a4e8d49b42c8615233a3443036d");
$agentMapper = new AgentMapper($adapter);
$agents = $agentMapper->findAll();

foreach ($agents as $agent) {
    echo "Name: " . $agent->name .  " - Status: " . $agent->status . "<br>";
}
Not bad at all, huh? The downside with this approach is that the whole refactoring process is way too drastic. And in more realistic use cases it won’t even be a viable solution (especially when dealing with large chunks of messy legacy code). Despite this, it shows in a nutshell how to create abstractions whose derivatives won’t break up the conditions imposed by the LSP, by using composition over inheritance, and with a pinch of Design by Contract also. Of course, let’s not forget the morale of this story: the hard fight against mean machines that enjoy enslaving humans and using them like plain batteries will hopefully end up in a resounding triumph. In Neo we trust. The end.

Closing Remarks (Outside the Matrix)

Being a central point of object-oriented design, and the “L” in the SOLID principles, the Liskov Substitution Principle has gained many angry detractors over the years, most likely because its academic definition is full of technical terms which blind sight and make hard to grasp what’s really behind its veil. Moreover, at face value it seems to contradict, or even condemn to an unavoidable doom of existence, of inheritance. But this is just a misleading impression which vanishes into thin air as soon as one gets the principle’s actual meaning. At its core, LSP’s commitment is aimed at designing hierarchies of classes that expose a real IS-A relationship with each other, and where subclasses can be replaced by their base abstractions without spoiling the contract with client code. So, make sure to stick to the principle’s commandments, and your life as a PHP developer (and hence your fat-wallet customers’) will be easier and way more enjoyable.

Frequently Asked Questions about the Liskov Substitution Principle

What is the Liskov Substitution Principle (LSP) in simple terms?

The Liskov Substitution Principle (LSP) is a concept in object-oriented programming that states that if a program is using a base class, it should be able to use any of its subclasses without the program knowing it. In simpler terms, it means that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program.

Why is the Liskov Substitution Principle important?

The LSP is crucial because it helps maintain the integrity of a system’s design. It ensures that a subclass can stand in for its superclass without causing any unexpected behavior. This principle is essential for the reusability of code, making it easier to maintain and extend over time.

Can you provide a practical example of the Liskov Substitution Principle?

Sure, let’s consider a simple example of a Rectangle class and a Square class. In geometry, a square is a rectangle, but if we define a Rectangle class with separate width and height and a Square class that extends the Rectangle but always keeps the width and height equal, we violate the LSP. If a function expecting a Rectangle, where it can independently change width and height, is given a Square, it will lead to unexpected behavior.

How does the Liskov Substitution Principle relate to other SOLID principles?

The LSP is the ‘L’ in the SOLID principles of object-oriented design. It closely relates to the other principles. For instance, it complements the Open-Closed Principle (OCP) which states that classes should be open for extension but closed for modification. By ensuring that subclasses can substitute for their superclasses, we can extend the system with new subclasses without modifying the existing code.

What happens if the Liskov Substitution Principle is violated?

Violating the LSP can lead to problems in your code, such as unexpected behavior and bugs that are hard to track. It can also make your code harder to maintain and extend, as the assumption that a subclass can substitute for its superclass no longer holds.

Is method overriding always a violation of the Liskov Substitution Principle?

Not necessarily. Method overriding is a common practice in object-oriented programming and it doesn’t always violate the LSP. However, if the overridden method changes the original method’s behavior in a way that is not expected from the superclass’s contract, it would violate the LSP.

How can I ensure that my code adheres to the Liskov Substitution Principle?

To ensure your code adheres to the LSP, you should make sure that subclasses can be substituted for their superclass without causing any unexpected behavior. This can be achieved by ensuring that subclasses fulfill the contract of the superclass. This includes not only the method signatures, but also their behavior.

Can the Liskov Substitution Principle be applied to interfaces?

Yes, the LSP can be applied to interfaces. Just like with classes, if a class implements an interface, any code that uses the interface should be able to use the implementing class without knowing it.

How does the Liskov Substitution Principle help in writing good unit tests?

The LSP is beneficial for unit testing because it allows you to substitute real classes with mock objects. If your code adheres to the LSP, you can be confident that the behavior of your code will not change when a superclass is replaced with a subclass.

Are there any tools or techniques to check if my code violates the Liskov Substitution Principle?

There are no specific tools to check for LSP violations, as it’s more of a design principle than a syntactic rule. However, good unit tests can help detect violations. If a test fails when a superclass is replaced with a subclass, it’s a sign of an LSP violation.

Alejandro GervasioAlejandro Gervasio
View Author

Alejandro Gervasio is a senior System Analyst from Argentina who has been involved in software development since the mid-80's. He has more than 12 years of experience in PHP development, 10 years in Java Programming, Object-Oriented Design, and most of the client-side technologies available out there.

Expert
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form