S.O.L.I.D - Practices for Object Oriented Programming, design better software with code examples in PHP

Last updated Apr 25, 2024 Published Apr 12, 2016

The content here is under the Attribution 4.0 International (CC BY 4.0) license

Being able to write better is an achievement that every programmer wants to. SOLID is a great thing to start with and particularly one of the best things to be followed by TDD. For years developers have been writing code and creating UML diagrams to represent what is known as “the design”. However, the design of the software is the source code itself, the code is the latest source of truth.

Often, the design is not quite what the business wants it to be to respond rapidly to changes and adapt to new requirements. This happens because the design did not anticipate the requirements change, but requirements are not to blame. An agile team invests in the design and adds the testing that corresponds. Martin defined a list with the signs that the software at hand has a broken design [1]:

  • Ridigity
  • Fragility
  • Immobility
  • Viscosity
  • Needless complexity
  • Needless repetition

The following video goes into details of each one of them:

To remedy such software rot, the SOLID principles were defined:

  • SRP - Single Responsibility Principle
  • OCP - Open Close Principle
  • LSP - Liskov Substitution Principle
  • ISP - Interface Segregation Principle
  • DIP - Dependency Inversion Principle

The remainder of this post will cover each of them in detail with code examples.

Single Responsibility

A class should have only a single responsibility (i.e. only one potential change in the software’s specification should be able to affect the specification of the class) [2] [3] [4].

<?php
class Upload {

    private $file;

    public function __construct($file)
    {
        $this->file = $file;
    }

    public function validate($data)
    {
        // apply validation rules
    }
       
    public function moveFile()
    {
        // move file
    }

    public function rename()
    {
        // rename file
    }
}

We have our class Upload doing too many things at once, named:

  • Validates the file
  • Move the file
  • Renaming the file
  • Lack of file object

A better approach to it is to create a new class to validate it, move it and finally handle it in a new place, let’s see some code:

<?php
class File {}

class ValidateFile {}

class MoveFile {}

class TransformFile {}

class Upload {

    private $file;

    public function __construct(File $file)
    {
        $this->file = $file;
    }

    public function validate($data)
    {
        // apply validation rules
    }
       
    public function moveFile()
    {
        // move file
    }

    public function rename()
    {
        // rename file
    }
}

Side note: Michael Feathers uses the SRP referring to improve code design in his talk [5].

Open/Close

software entities … should be open for extension, but closed for modification [2] [3] [4].

This principle is followed by many vendors to allow developers to extend features without changing the source code. Let’s first understand the problem we want to solve.

Let’s imagine we have a class to send data through a web service and at the time we have only the XML handler to send this but your client requested you to implement the feature to support JSON, what would you do?

<?php
class Webservice {

    public function sendXml($data) {
        // handle the send here
    }
}

In the code above we have unfortunately only one choice, add a method sendJson, but that would not follow the Open/Close principle. With this action, we would have to change the source code to add a new feature (should be open for extension, but closed for modification). How can we fix this code?

<?php
abstract class Type {}

class XML extends Type {}

class XML extends Json {}

class Webservice {

    public function send(Type $data) {
        // handle the send here
    }
}

To easily refactor this code accordingly with the open/close principle we would first create a new generic class called Type, this class will abstract the XML and JSON type, which we can then give to the send date as a parameter.

<?php

$type = new Json();

$webService = new Webservice();
$webService->send($json);

This approach is much cleaner and easier to create a new extension to it, we can extend the Type class and create as many types as we want.

Liskov Substitution

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program [2] [3] [4].

This principle is connected with inheritance one of the most common features used in Object Oriented Programming. The principle says that the subtype should not alter the result of the program. In the following example we have a class called Door, inside what we have is a simple method that tells us when the door is open or not.

<?php

class Door {

    protected $open = true;

    public function isOpen()
    {
        return $open;
    }

    public function open()
    {
        $this->open = true;
    }

    public function close()
    {
        $this->open = false;
    }
}

A simple class and easy to use, then we have our client code, which uses that class

<?php

class Client {

    public function execute(Door $door)
    {
        if ($door->isOpen()) {
            $door->close();
        }
    }
}

$client = new Client();
$client->execute(new Door());

Now let’s say we have another door but this time this door is a decorated one.

<?php

class DecoratedDoor extends Door {

    public function isOpen()
    {
        if (!parent::isOpen()) {
            throw new \Exception('Door is closed');
        }
    }
}

The code above is valid but if the client decides to use the DecoratedDoor his code will break. We are not following the Liskov principle “Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”.

<?php

// Original code
$client = new Client();
$client->execute(new Door());

/**
 * The client can't use the DecoratedDoor because it throws an Exception
 */
$client = new Client();
$client->execute(new DecoratedDoor());

In other words to fix this problem and follow the Liskov principle we need just provide for the client class the same result that he expects from the Door class, true or false.

Interface Segregation

Many client-specific interfaces are better than one general-purpose interface [2] [3] [4].

This I think is the easier one to understand, the quote above is really clear. So let’s dive into the source code and get our hands dirty.

<?php
interface Bird {

    public function fly();

    public function eat();
}

The interface above has two methods for our Bird, and we can implement this interface easy and well, as the code below shows:

<?php
class ConcreteBird implements Bird {

    public function fly()
    {
        print 'I can fly';
    }

    public function eat()
    {
        print 'I can eat';
    }
}

This kind of generalization works fine and there is just one problem with it, what if I have a bird who doesn’t fly or doesn’t eat?

We do not have many options though, we have to implement all methods, therefore what we can do to identify when a bird can’t fly or eat is thrown an exception.

<?php
class ConcreteBirdWithoutFly implements Bird {

    public function fly()
    {
        throw new \Exception('I cannot fly');
    }

    public function eat()
    {
        print 'I can eat';
    }
}

class ConcreteBirdWithoutEat implements Bird {

    public function fly()
    {
        print 'I can fly';
    }

    public function eat()
    {
        throw new \Exception('I cannot eat');
    }
}

For many of us, it’s just ok to do this, but the truth is that we are implementing a method that we don’t need. Then comes Interface Segregation to save us, “Many client-specific interfaces are better than one general-purpose interface”. To deal with that we simply create a new interface with the method eat.

<?php
interface Bird {

    public function fly();
}

interface Eatable {

    public function eat();
}

With this approach, we can finally implement the specific interface instead of just throwing exceptions if the class doesn’t support the contract. The following code supports both Bird and Eatable.

<?php

class ConcreteBirdWithoutEat implements Bird, Eatable {

    public function fly()
    {
        print 'I can fly';
    }

    public function eat()
    {
        print 'I can eat';
    }
}

This code is more flexible as well, we just implement what we are going to need.

Dependency inversion

One should “Depend upon Abstractions. Do not depend upon concretions” [2] [3] [4].

The following code just clarifies the idea of abstraction and how to depend on it instead of a concrete instance.

<?php

abstract class Drivable {}

class Driver extends Drivable {}

class Car {

    public function run(Drivable $driver) {}
}

The following approach allows us to implement as many drivers as we want and give it as a parameter to the run method. You can complain that we could easily depend directly on Driver, but if we do that we will block the flexibility of the code.

References

  1. [1]R. C. Martin, Agile software development: principles, patterns, and practices. Prentice Hall PTR, 2003.
  2. [2]Wikipedia, “SOLID,” 2021 [Online]. Available at: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design). [Accessed: 03-Apr-2021]
  3. [3]S. Oloruntoba, “SOLID: The First 5 Principles of Object Oriented Design,” 2020 [Online]. Available at: https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design. [Accessed: 17-Dec-2020]
  4. [4]D. Bailey, “S.O.L.I.D. Software Development, One Step at a Time,” 2020 [Online]. Available at: https://www.codemag.com/article/1001061. [Accessed: 28-Sep-2020]
  5. [5]M. Feathers, “the deep synergy between testability and good design,” 2013 [Online]. Available at: https://www.youtube.com/watch?v=4cVZvoFGJTU. [Accessed: 23-May-2021]