The Liskov Substitution Principle

Alejandro Gervasio
Share

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.