Getting Familiar with SOLID Principles in OOP

SOLID is an acronym for a couple of principles that any piece of software written based on object-oriented design needs to follow and in this tutorial, we are going to see what SOLID is and how we can implement it.

SOLID, a set of rules and standards, stands for the following expressions: 

- Single responsibility
- Open/closed 
- Liskov substitution
- Interface segregation
- Dependency inversion  

In order to make the examples used in this tutorial more meaningful, I haven't followed the above rules in turn; that's why I've started the discussion with Interface segregation in paragraphs ahead (To these rules and principles, I've used PHP programming languages in this tutorial.)

Interface segregation

This principle helps design good classes. Based on this principle, a class should depend on the smallest set of interface features; in other words, the fewest methods and attributes. If an interface has too many methods, the implementing class is bound to a bunch of methods it doesn't need at all! Let’s take a look at an example of Interface segregation violation: 

<?php
interface MessageInterface
{
    public function add(string $str);
    public function notify();
}

class Message implements MessageInterface
{
    public function add(string $str)
    {
        return "{$str} is added";
    }

    public function notify()
    {}
}

$messageObj = new Message();
echo $messageObj->add('Hi');

In this example, there is just one interface including two methods and the implementing class is forced to add the notify() method as well (No need to say that when we're implementing an interface, we must add all methods defined in the interface.) In situations like this, we can simply define multiple interfaces and our class can implement them if needed. A better solution that complies with this principle is as follows:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

class Message implements MessageInterface
{
    public function add(string $str)
    {
        return "{$str} is added";
    }
}

$messageObj = new Message();
echo $messageObj->add('Hi');

If any class might need both methods, it can easily implement both MessageInterface and NotificationInterface interfaces.

Dependency inversion

This principle says that high-level modules should not depend on low-level modules, instead both should depend on abstractions and abstractions should not depend on details. We have completed the previous example in a way that violates DIP as follows:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

class Message implements MessageInterface
{
    public function add(string $str)
    {
        $database = new Database();
        $database->query($str);
    }
}

In the add() method, we have created an object of Database class. Although it works, this methodology doesn't comply with this principle and needs to be refactored as follows:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

interface DatabaseInterface
{
    public function query();
}

class Database implements DatabaseInterface
{
    public function query()
    {
        return "The query is sent to DB";
    }
}

class Message implements MessageInterface
{
    private $db;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database;
    }

    public function add(string $str)
    {
        $this->db->query($str);
        return "{$str} is added in the database";
    }
}

$messageObj = new Message(new Database());
echo $messageObj->add('Hi');

By using DIP, whenever we want to create an object from Message class, an instance of Database class must be passed to the constructor of Message class; this way our Message class no longer is dependent on Database class

Single responsibility

This principle says that each and every class should have just one reason to change; in other words, our classes should do one thing and do it well. Let's imagine we want to log all user activities in case of any exception:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

interface DatabaseInterface
{
    public function query();
}

class Database implements DatabaseInterface
{
    public function query()
    {
        return "The query is sent to DB";
    }
}

class Message implements MessageInterface
{
    private $db;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database;
    }

    public function add(string $str)
    {
        try {
            $this->db->query($str);
            return "{$str} is added in the database";
        } catch (Exception $exception) {
            $this->db->query($exception->getMessage());
        }
    }
}

$messageObj = new Message(new Database());
echo $messageObj->add('Hi');

At the same time, the add() method handles adding a new message as well as logging exceptions if any but this class violates the SRP; to comply with this principle, we can modify the above code as follows:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

interface DatabaseInterface
{
    public function query();
}

interface LoggerInterface
{
    public function add(sting $log);
}

class Database implements DatabaseInterface
{
    public function query()
    {
        return "The query is sent to DB";
    }
}

class Logger implements LoggerInterface
{
    private $db;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database;
    }

    public function add(sting $log)
    {
        $this->db->query($log);
    }
}

class Message implements MessageInterface
{
    private $db;

    public function __construct(DatabaseInterface $database, LoggerInterface $logger)
    {
        $this->db = $database;
    }

    public function add(string $str)
    {
        try {
            $this->db->query($str);
            return "{$str} is added in the database";
        } catch (Exception $exception) {
            $logger->add($exception->getMessage());
        }
    }
}

$database = new Database();
$logger = new Logger($database);
$messageObj = new Message($database, $logger);
echo $messageObj->add('Hi');

Now the responsibility of logging is handled in another class that is specifically created to do a single task of adding logs into the database. 

Open/closed 

This principle states that a good class is open to extension but closed to modification which may sound a little bit confusing but by making an example, it will be way easier to grasp:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

interface DatabaseInterface
{
    public function query();
}

interface LoggerInterface
{
    public function add(sting $log);
}

class Database implements DatabaseInterface
{
    public function query()
    {
        return "The query is sent to DB";
    }
}

class Logger implements LoggerInterface
{
    private $db;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database;
    }

    public function add(sting $log)
    {
        $this->db->query($log);
    }
}

class Message implements MessageInterface
{
    private $db;

    public function __construct(DatabaseInterface $database, LoggerInterface $logger)
    {
        $this->db = $database;
    }

    public function add(string $str)
    {
        try {
            if (strpos($str, '#') === 0) {
                $this->db->query($str);
                return "{$str} is added as a hashtag in the database";
            } else {
                $this->db->query($str);
                return "{$str} is added in the database";
            }
        } catch (Exception $exception) {
            $logger->add($exception->getMessage());
        }
    }
}

$database = new Database();
$logger = new Logger($database);
$messageObj = new Message($database, $logger);
echo $messageObj->add('#PHP');

Inside try block, an if statement is used to check if the message starts with a hashtag; If so, the code inside the if block will be run otherwise the interpreter goes to the else block.

The Open/closed principle says that all classes and functions should be open for extensions but closed for modification and the above code violates this principle because it is open for modification. What if we need to add another else if statement to check whether the message starts with an addsign? We can make this code compliant with the Open/closed principle via inheritance as follows:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

interface DatabaseInterface
{
    public function query();
}

interface LoggerInterface
{
    public function add(sting $log);
}

class Database implements DatabaseInterface
{
    public function query()
    {
        return "The query is sent to DB";
    }
}

class Logger implements LoggerInterface
{
    private $db;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database;
    }

    public function add(sting $log)
    {
        $this->db->query($log);
    }
}

class Message implements MessageInterface
{
    protected $db;

    public function __construct(DatabaseInterface $database, LoggerInterface $logger)
    {
        $this->db = $database;
    }

    public function add(string $str)
    {
        try {
            $this->db->query($str);
            return "{$str} is added in the database";
        } catch (Exception $exception) {
            $logger->add($exception->getMessage());
        }
    }
}

class Hashtag extends Message
{
    public function add(string $str)
    {
        $this->db->query($str);
        return "{$str} is added as a hashtag in the database";
    }
}

class Mention extends Message
{
    public function add(string $str)
    {
        $this->db->query($str);
        return "{$str} is added as a mention in the database";
    }
}

$database = new Database();
$logger = new Logger($database);
$message = "@PHP";
if (strpos($message, '#') === 0) {
    $hashtagObj = new Hashtag($database, $logger);
    echo $hashtagObj->add($message);
} else if (strpos($message, '@') === 0) {
    $mentionObj = new Mention($database, $logger);
    echo $mentionObj->add($message);
} else {
    $messageObj = new Message($database, $logger);
    echo $messageObj->add($message);
}

Through inheritance, it is now much easier to add new features by overriding the add() method (The evaluation of the first character # or @ will be handled elsewhere at a higher level of our web application.)

Liskov substitution

Last but not least is Liskov substitution principle which took me a lot to understand than all of the other SOLID principles combined. This principle emphasizes that objects of any super-class can be replaced with objects of any sub-class and help programmers design good polymorphism. In other words, the behavior of a sub-class should be as correct as of the behavior of the super-class as follows:

<?php
interface MessageInterface
{
    public function add(string $str);
}

interface NotificationInterface
{
    public function notify();
}

interface DatabaseInterface
{
    public function query();
}

interface LoggerInterface
{
    public function add(sting $log);
}

class Database implements DatabaseInterface
{
    public function query()
    {
        return "The query is sent to DB";
    }
}

class MySQL extends Database
{

}

class Logger implements LoggerInterface
{
    private $db;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database;
    }

    public function add(sting $log)
    {
        $this->db->query($log);
    }
}

class Message implements MessageInterface
{
    protected $db;

    public function __construct(DatabaseInterface $database, LoggerInterface $logger)
    {
        $this->db = $database;
    }

    public function add(string $str)
    {
        try {
            $this->db->query($str);
            return "{$str} is added in the database";
        } catch (Exception $exception) {
            $logger->add($exception->getMessage());
        }
    }
}

class Hashtag extends Message
{
    public function add(string $str)
    {
        $this->db->query($str);
        return "{$str} is added as a hashtag in the database";
    }
}

class Mention extends Message
{
    public function add(string $str)
    {
        $this->db->query($str);
        return "{$str} is added as a mention in the database";
    }
}

$database = new MySQL();
$logger = new Logger($database);
$message = "@PHP";
if (strpos($message, '#') === 0) {
    $hashtagObj = new Hashtag($database, $logger);
    echo $hashtagObj->add($message);
} else if (strpos($message, '@') === 0) {
    $mentionObj = new Mention($database, $logger);
    echo $mentionObj->add($message);
} else {
    $messageObj = new Message($database, $logger);
    echo $messageObj->add($message);
}

As you can see, in line 30 a new class called MySQL is created which extends Database class and in line 88 the $database variable is made of MySQL class but the code is still functioning properly because we're complying with LSP. In other words, any code calling methods on objects of the Database class continues to work when those objects get replaced with instances of MySQL class which is a subtype of Database class.

Conclusion
These five aren't the only OO design principles and three others are:

- Don t Repeat Yourself or DRY
- General Responsibility Assignment Software Principle or GRASP
- Test Driven Development or TDD

The DRY principle asks developers not to have any duplicate code. GRASP is a collection of ideas that inspire single responsibility. TDD refers to an approach that the code written should be easily testable.

by Behzad Moradi on 2019-10-29

Login to add your comment