Understanding the SOLID principles of software engineering

1. Introduction

Software engineering is a combination of art and science. In this article, we will understand the most basic and essential software design principle: SOLID, which is an acronym for five different principles given by Robert C. Martin. He is famously known as Uncle Bob. Software designed using these principles is easily extensible, maintainable, reusable and better suited to the adaptive or agile style of business needs. Following these principles also help to avoid code smells and leaves the code to be easily refactor-able. Followings are the five principles:

S – Single-responsibility principle
O – Open-closed principle
L – Liskov substitution principle
I – Interface segregation principle
D – Dependency Inversion Principle

Let’s dive deep into these principles one by one.

2. Single-responsibility Principle

This principle states:

A class should have one and only one reason to change, meaning that a class should have only one responsibility.

In other words, it implies that a class should be responsible for only one job and whenever there is a change in that job, it should be the only reason to change that class.

For example, say we have some planets, and we want to compute the surface area of planets. The surface area depends on the radius. So, let’s assume the following classes:

interface Planet{
}
class Earth implements Planet {
    public $radius;

    public function construct($radius) {
        $this->radius = $radius;
    }
}
class Mercury implements Planet{
    public $radius;

    public function construct($radius) {
        $this->radius = $radius;
    }
}

First, we create our classes and have the constructors to set up the required parameters. Next, we are creating a class which calculates the surface area.

class SurfaceAreaCalculator {

    protected $planet;

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

    public function getSurfaceArea() {
        $radius = $this->planet->radius;
        return 4*(22/7)*$radius*$radius;
    }

    public function output() {
        return '<p>'. 'the surface area of planet is '. $this->getSurfaceArea() . "</p>"
    }
}

To compute the surface area of any planet, we can instantiate the class SurfaceAreaCalculator and pass an object of type planet.

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Now, the SurfaceAreaCalculator is responsible for two things – first calculating the surface area and second outputting that. What if tomorrow we want to use JSON or XML output from output method? Then we need to change the class.

So, to fix this, we can create an AreaOutputFormatter class and use this class to print the output in the format we want.

class AreaOutputFormatter{
    private $area;

    public function construct($area) {
        $this->area = $area;
    }

    public function HTML(){
        return '<p>'. 'the surface area of planet is '. $this->area. '<p>';
    }

    public function Blade(){
        return '<p>'. 'the surface area of planet is '. $this->area;
    }

}

The AreaOutputFormatter class would work like this:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$surfaceArea = $calc->getSurfaceArea();
$output = new AreaOutputFormatter($surfaceArea);
echo $output->HTML();
echo $output->Blade();

3. Open-closed Principle

The second principle states:

Objects or entities should be open for extension but closed for modification.

It implies that a class should be easily extendable without modifying the class itself. Let’s have a look on AreaOutputFormatter; it supports HTML and Blade output but what if we want to have output in JSON? Now, we need to modify our existing class, which is terrible.

To make AreaOutputFormatter open for extension and closed for modification, we should refactor our code as below:

interface IAreaOutputFormatter{
    public function getOutput();
}

Now, we should create implementing classes of this interface like:

class HTMLAreaOutputFormatter implements IAreaOutputFormatter {
    private $area;

    public function construct($area) {
        $this->area = $area;
    }

    public function getOutput(){
        return '<p>'. 'the surface area of planet is '. $this->area. '<p>';
    }

}

The above class implements output in HTML format. Similarly, we can create one more class which outputs the area in JSON:

class JSONAreaOutputFormatter implements IAreaOutputFormatter {
    private $area;

    public function construct($area) {
        $this->area = $area;
    }

    public function getOutput(){
        return json_encode (["area" => $this->area]);
    }

}

In this way, we can extend our output format class to many output types.

4. Liskov substitution principle

The Liskov substitution principle states:

Every subclass/derived class should be substitutable for their base/parent class.

Assume a new class LiveablePlanet, which extends class Earth:

class LiveablePlanet extends Earth{
    public function color(){
    }
}

Now according to the Liskov substitution principle, we should be able to use LivablePlanet class wherever we are using Earth.

Like in the above example, this should work:

$planet = new LivablePlanet (6371); // here earlier we used Earth class
$calc = new SurfaceAreaCalculator($planet);
$surfaceArea = $calc->getSurfaceArea();
$output = new AreaOutputFormatter($surfaceArea);
echo $output->HTML();
echo $output->Blade();

5. Interface segregation principle

This principle states:

A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.

Using our Planet interface above, assume that we add a method named getHighestMountainRange():

interface Planet{
    public function getHighestMountainRange();
}

However, the problem with this approach is interface Planet forces Earth and Mercury class to implement the method getHighestMountainRange().

A better and cleaner approach would be to create a new interface like MountainyPlanet, which extend the Planet interface and then Earth should implement MountainyPlanet because Earth has mountains and Mercury should implement Planet (assuming Mercury have no mountains).

Our code outlines would look like:

interface Planet{
}
interface MountainyPlanet extend Planet{
    public function getHighestMountainRange();
}
class Mercury implements Planet{
}
class Earth implements MountainyPlanet{
    public function getHighestMountainRange(){
        return "Himalayas";
    }
}

6. Dependency Inversion principle

The last, Dependency Inversion principle states :

Different entities must depend on abstractions, not on concretions.
It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.

In simple words: This principle allows for decoupling, an example that seems like the best way to explain this principle:

class PasswordReminder {
    private $dbConnection;

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

}

MySQLConnection is the low-level module and PasswordReminder is high level and PasswordReminder is being forced to depend on MySQLConnection.

If in future, we want to switch to Oracle in place of MySQL, we have to edit PasswordReminder class which violets Open-close principle.

The PasswordReminder class should not be dependent on the database system that we are using in our application. As high level and low-level modules should connect through an interface, let’s create an interface:

interface DBConnectionInterface {
    public function connect();
}

This interface has a connect method and classes implementing this interface should provide an implementation for connect() method.
Now, we can modify MySQLConnection class to implement DBConnectionInterface, and in the constructor of PasswordReminder we accept an object of type DBConnectionInterface, our code looks like:

class MySQLConnection implements DBConnectionInterface {
    public function connect() {
        return "Database connection";
    }
}
class PasswordReminder {
    private $dbConnection;

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

}

Now, if in future if we want to use Oracle, we can quickly create class MySQLConnection which implements DBConnectionInterface. The flexibility of database independence become possible by creating a common interface DBConnectionInterface which connects the high-level modules and low-level modules.

7. Conclusion

Bingo! We learned about SOLID design patterns in this article, and with continuous usage and application in daily life, it becomes the part of our code which results in beautiful code which can be easily be modified, extended, tested, and refactored without any problems. If you are more interested in diving deep into design patterns, you might want to purchase Design Patterns: Elements of Reusable Object-Oriented Software


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *