DEV Community

Cover image for The five SOLID principles and why you should use them in your codebase
Tom Green
Tom Green

Posted on • Updated on • Originally published at tomgreen.dev

The five SOLID principles and why you should use them in your codebase

SOLID outlines a group of guidelines that developers can use to simplify and clarify their code. While they are certainly not laws, understanding these concepts will make you a better developer. In overview, the five SOLID principles are:

  • Single-responsibility principle; A class should only have a single responsibility, that is, only changes to one part of the software's specification should be able to affect the specification of the class.
  • Open–closed principle; Your classes should be open for extension but closed for modification.
  • Liskov substitution principle; Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
  • Interface segregation principle; Many client-specific interfaces are better than one general-purpose interface.
  • Dependency inversion principle; One should depend upon abstractions, not concretions.

Don't worry if you don't fully understand the meaning of these for the first time of reading - I didn't. I will now go over each principle one by one, and try to explain not just how they work, but how they will benefit you in the long run.

Single-responsibility principle

The most common of the SOLID design principles, the single responsibility principle, states that a class should have only one reason to change. When a class handles more than one responsibility, any changes made to the functionalities may propagate throughout the application in unexpected ways. This unexpected behaviour can be detrimental if you have a smaller application but can become even worse when you work with large, enterprise-level software. By making sure that each function only encapsulates only one responsibility, you can save a lot of testing time and create a more maintainable architecture.

Let me show you an example. I will use PHP but you can apply SOLID design principles to any other OOP languages, too.

Let's imagine we have a class that represents a text document, where said document has a title and content. This document must be able to be exported to HTML and PDF.

<?php

class Document
{
    protected $title;
    protected $content;

    public function __construct(string $title, string $content)
    {
        $this->title = $title;
        $this->content= $content;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getContent(): string
    {
        return $this->content;
    }

    public function exportHtml() {
        echo "DOCUMENT EXPORTED TO HTML".PHP_EOL;echo "Title: ".$this->getTitle().PHP_EOL;
        echo "Content: ".$this->getContent().PHP_EOL.PHP_EOL;
    }

    public function exportPdf() {
        echo "DOCUMENT EXPORTED TO PDF".PHP_EOL;
        echo "Title: ".$this->getTitle().PHP_EOL;
        echo "Content: ".$this->getContent().PHP_EOL.PHP_EOL;
    }
}

Enter fullscreen mode Exit fullscreen mode

In this case, it is not the responsibility of the document to export itself to a particular format, the document should only be a representation of itself.

The key to solving this is to move each of the export methods into their own classes, which will implement an "Exportable" interface.

<?php
interface ExportableDocumentInterface
{
    public function export(Document $document);
}
Enter fullscreen mode Exit fullscreen mode

The next thing we have to do is extract the logic that does not apply to the class.

<?php
class PdfExportableDocument implements ExportableDocumentInterface
{
    public function export(Document $document)
    {
        echo "DOCUMENT EXPORTED TO PDF".PHP_EOL;
        echo "Title: ".$document->getTitle().PHP_EOL;
        echo "Content: ".$document->getContent().PHP_EOL.PHP_EOL;
    }
}

class HtmlExportableDocument implements ExportableDocumentInterface
{
    public function export(Document $document)
    {
        echo "DOCUMENT EXPORTED TO HTML".PHP_EOL;
        echo "Title: ".$document->getTitle().PHP_EOL;
        echo "Content: ".$document->getContent().PHP_EOL.PHP_EOL;
    }
}

Enter fullscreen mode Exit fullscreen mode

Leaving the document class something like this

<?php
class Document
{
    protected $title;
    protected $content;

    public function __construct(string $title, string $content)
    {
        $this->title = $title;
        $this->content= $content;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getContent(): string
    {
        return $this->content;
    }
}
Enter fullscreen mode Exit fullscreen mode

Open–closed principle

The open-closed principle states that objects or entities should be open for extension, but closed for modification. This is one of the principles that developers often skip over - but try not to. These techniques are paramount to mature design.

So, you should be able to extend your existing code using features like inheritance via subclasses and interfaces. However, you should never modify classes, interfaces, and other code units that already exist, as it can lead to unexpected behaviour. If you add a new feature by extending your code rather than modifying it, you reduce the risk of failure as much as possible.

Let's imagine that we need to achieve a login system. To authenticate our user we require a username and a password, so far so good. So what happens a year later if we want the ability for a user to authenticate through Twitter or Facebook? It is important to understand that what has been asked of us is not a change to a current feature, but rather to build a new feature.

Let's say our authentication class looks like this, where you call an authenticate method for your user.

<?php
class LoginService
{
    public function login($user)
    {
        $this->authenticateUser($user);
    }
}

Enter fullscreen mode Exit fullscreen mode

When it comes to implementing our third-party user, we may want to try something like this, where we check what type of user we have using an if statement, and executing code accordingly.

<?php
class LoginService
{
    public function login($user)
    {
        if ($user instanceof User) {
            $this->authenticateUser($user);
        } else if ($user instanceOf ThirdPartyUser) {
            $this->authenticateThirdPartyUser($user);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

This isn't good because we are modifying code that is already in place. It may look good now, but what happens when you are supporting five or six authentication types? Instead, you should abstract and work to an interface. The first thing we should do is build an interface that complies with what we want to do for the specific use case.

<?php
interface LoginInterface
{
    public function authenticateUser($user);
}

Enter fullscreen mode Exit fullscreen mode

Now we can decouple the logic that we had already created for our use case, then implement a class using our new interface.

<?php

class UserAuthentication implements LoginInterface
{
    public function authenticateUser($user)
    {
        // TODO: Implement authenticateUser() method.
    }
}

Class ThirdPartyUserAuthentication implements LoginInterface
{
    public function authenticateUser($user)
    {
        // TODO: Implement authenticateUser() method.
    }
}
Enter fullscreen mode Exit fullscreen mode

Now our LoginService class doesn't care what type of user we have, it just interacts with a 'LoginInterface'.

<?php
class LoginService
{
    public function login(LoginInterface $user)
    {
        $user->authenticateUser($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Liskov Substitution

Coined by Barbara Liskov, this principle states that any implementation of an abstraction (interface) should be substitutable in any place that abstraction is accepted.

In layman’s terms, it states that an object of a parent class should be replaceable by objects of its child class without causing issues in the application. So, a child class should never change the characteristics of its parent class (such as the argument list and return types). You can implement the Liskov Substitution Principle by paying attention to the correct inheritance hierarchy.

Let's say we have a Shipping class that is going to calculate the shipping cost of a product given its weight and destination.

<?php
class Shipping
{
    public function calculateShippingCost($weightOfPackageKg, $destiny)
    {
        // Pre-condition:
        if ($weightOfPackageKg <= 0) {
            throw new \Exception('Package weight cannot be less than or equal to zero');
        }

        // We calculate the shipping cost by
        $shippingCost = rand(5, 15);

        // Post-condition
        if ($shippingCost <= 0) {
            throw new \Exception('Shipping price cannot be less than or equal to zero');
        }

        return $shippingCost;
    }
}
Enter fullscreen mode Exit fullscreen mode

But for worldwide shipping, we want these rules to be slightly different, so we create a child class that extends the Shipping class.

<?php
class WorldWideShipping extends Shipping
{
    public function calculateShippingCost($weightOfPackageKg, $destiny)
    {
        // Pre-condition
        if ($weightOfPackageKg <= 0) {
            throw new \Exception('Package weight cannot be less than or equal to zero');
        }

        // We strengthen the pre-conditions
        if (empty($destiny)) {
            throw new \Exception('Destiny cannot be empty');
        }

        // We calculate the shipping cost by
        $shippingCost = rand(5, 15);

        // By changing the post-conditions we allow there to be cases
        // in which the shipping is 0
        if ('Spain' === $destiny) {
            $shippingCost = 0;
        }

        return $shippingCost;
    }
}
Enter fullscreen mode Exit fullscreen mode

The problem here is that the worldwide shipping method does not provide the same implementation, as seen by $destiny now throwing an exception if empty.

The best way not to break LSP is by using Interfaces. Instead of extending our child classes from a parent class.

<?php
interface CalculabeShippingCost
{
    public function calculateShippingCost($weightOfPackageKg, $destiny);
}
Enter fullscreen mode Exit fullscreen mode
<?php
class WorldWideShipping implements CalculabeShippingCost
{
    public function calculateShippingCost($weightOfPackageKg, $destiny)
    {
        // Implementation of logic
    }
}
Enter fullscreen mode Exit fullscreen mode

By using interfaces you can implement methods that different classes have in common, but each method will have its own implementation, its own pre and post conditions etc. We are not tied to a parent class.

Interface Segregation Principle

The Interface Segregation Principle states that a client should never be forced to implement an interface that it doesn’t use. As you'll find, this all comes down to knowledge.

The breach of Interface Segregation Principle harms code readability and requires programmers to write empty stub methods that do nothing. In a well-designed application, you should avoid interface pollution (also called fat interfaces). The solution is to create smaller interfaces that you can implement more flexibly.

Say we have a class that represents a hardcover book, and another class that represents an audiobook. We want to create an interface that represents the actions a user can do with this book.

<?php
interface BookAction {

    public function seeReviews();

    public function searchSecondhand();

    public function listenSample();

}
Enter fullscreen mode Exit fullscreen mode

Now if we were to add this implementation to our classes, both of these classes now have to contain methods that are not relevant to them. The HardcoverBook cannot have a sample to listen to, for example. Similarly, audiobooks don’t have second-hand copies, so the AudioBook class doesn’t need it either.

<?php
class HardcoverBook implements BookAction {

    public function seeReviews() {...}

    public function searchSecondhand() {...}

    public function listenSample() {...}

}

class AudioBook implements BookAction {

    public function seeReviews() {...}

    public function searchSecondhand() {...}

    public function listenSample() {...}

}

Enter fullscreen mode Exit fullscreen mode

However, as the BookAction interface includes these methods, all of its dependent classes have to implement them. In other words, BookAction is a polluted interface that we need to segregate. Let’s extend it with two more specific interfaces: HardcoverAction and AudioAction.

<?php

    interface BookAction {
        public function seeReviews();
    }

    interface HardcoverAction extends BookAction {
        public function searchSecondhand();
    }

    interface AudioAction extends BookAction {
        public function listenSample();
    }

Enter fullscreen mode Exit fullscreen mode

Now the HardcoverBook class can implement the HardcoverAction interface and the AudioBook class can implement the AudioAction interface. This way, both classes can implement the seeReviews() method of the BookAction super-interface. However, HardcoverBook doesn’t have to implement the irrelevant listenSample() method and AudioBook doesn’t have to implement searchSecondhand(), either.

Dependency Inversion Principle

The Dependency Inversion Principle states that high-level modules should never depend on low-level modules, instead the high-level module can depend upon an abstraction and the low-level module depends on that same abstraction. It’s not the simplest statement that we have come across. In very simple words… nope, a statement this complex can’t be simplified.

Let's look at an example, take this PasswordReminder class. We pass in a MySQLConnection to the construct. This might look legit, but this is breaking the dependency inversion principle. The high-level class (PasswordReminder) now relies on the low-level class (MySQLConnection).

<?php
class PasswordReminder {

    protected $dbConnection;

    public function __construct(MySQLConnection $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}
Enter fullscreen mode Exit fullscreen mode

So what do we do to fix this? Well, we code to an interface. You may have noticed by now that interfaces are very useful tools for following the SOLID principles. So if we set up a ConnectionInterface, it can have a collect method.

<?php

interface ConnectionInterface()
{
    public function connect();
}

Enter fullscreen mode Exit fullscreen mode

Now if we were to follow the principle, we should change the PasswordReminder class to use this Interface instead of the implementation of the interface.

<?php

class PasswordReminder {

    protected $dbConnection;

    public function __construct(ConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}
Enter fullscreen mode Exit fullscreen mode

The purpose of applying the principles in software projects is to take advantage of the benefits of using object-oriented paradigm correctly, avoiding problems such as a lack of code standardisation, and duplication of code. And if we can follow all these tips, we will have an easy code to maintain, test, reuse, and extend. As a next step, start training on personal, small and simpler projects. You can start by making changes in specific classes. Soon, you will begin to train your brain to think more maturely when faced with more complex development situations.

Top comments (2)

Collapse
 
jwp profile image
John Peters • Edited

What's not well-known in the Javascript world is that instead of Classes the same principles apply to functions.

In Javascript, functions are true first class objects, just like a class object.

Because of this, we may easily adopt SOLID for our functional programming style. I call this Polymorphic Composition as described here.

Collapse
 
kvuluong profile image
kvu-luong • Edited

With the second principle, Could you give me the simple code for apply this method. I had try it by my self. However, I'm not figure it out how it work. These image below show the code I implemented. How to write code to checking authentication in index.php. Link dev-to-uploads.s3.amazonaws.com/i/...