SOLID – the first five principles of Object Oriented Design

SOLID: what is it?

Robert Cecil Martin is the author of First five principles of object oriented design. Learn more Robert Cecil Martin.

These object oriented design principles are necessary to build robust, easy-to-maintain and extend code.

The acronym S.O.L.I.D was introduced later by Michael Feathers:

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

Let’s see an example of application of each of these principles to understand how they can facilitate the development of high quality applications.

Single responsibility principle

A class should have only one reason to change

A reformulation

A class or module should have one, and only one, the reason to be changed

What does it mean concretely? ???

A class does not have to do everything. It must have one and only one responsibility.

As example, consider the following class:

<?php

namespace App\Infrastructure;

use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerInterface;

/**
 * Class GalleryDriver
 */
final class GalleryDriver implements GalleryDriverInterface
{
    /** @var ClientInterface */
    private $client;

    /** @var LoggerInterface */
    private $logger;

    /**
     * GalleryDriver constructor.
     *
     * @param ClientInterface $client
     * @param LoggerInterface $logger
     */
    public function __construct(ClientInterface $client, LoggerInterface $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    /**
     * @inheritDoc
     */
    public function findAll(array $options): array
    {
        $uri = sprintf('https://picsum.photos/v2/list?%s', http_build_query($options));

        try {
            $response = $this->client->request('GET', $uri);

            $contents = $response->getBody()->getContents();
        } catch (\Exception $exception) {
            $this->logger->error('Error: ' . $exception->getMessage());

            throw $exception;
        }

        return \json_decode($contents);
    }
}
<?php

namespace App\Infrastructure;

interface GalleryDriverInterface
{
    /**
     * param array $options
     *
     * @return array
     *
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function findAll(array $options): array;
}

The method findAll(array $options) has the following roles:

  • build request uri
  • call api
  • get response contents

It has many responsibility, this violates the single responsibility principle.

To optimize, we will limit the responsibility of the method findAll(array $options)

Now

  • GalleryDriver has just one and only one responsibility : get response
  • UriBuilder has just one and only one responsibility : build request uri
  • ApiClient has just one and only one responsibility : call API and get response content

See source code below:

<?php

namespace App\Infrastructure;

/**
 * Class GalleryDriver
 */
final class GalleryDriver implements GalleryDriverInterface
{
    /** @var ApiClientInterface */
    private $client;

    /** @var UriBuilderInterface */
    private $uriBuilder;

    /**
     * GalleryDriver constructor.
     *
     * @param ApiClientInterface $client
     * @param UriBuilderInterface $uriBuilder
     */
    public function __construct(ApiClientInterface $client, UriBuilderInterface $uriBuilder)
    {
        $this->client = $client;
        $this->uriBuilder = $uriBuilder;
    }

    /**
     * @inheritDoc
     */
    public function findAll(array $options): array
    {
        $contents = $this->client->retrieve($this->uriBuilder->build($options));

        return \json_decode($contents);
    }
}
<?php

namespace App\Infrastructure;

use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerInterface;

/**
 * Class ApiClient
 */
final class ApiClient implements ApiClientInterface
{
    /** @var ClientInterface */
   private $client;

   /** @var LoggerInterface */
   private $logger;

    /**
     * ApiClient constructor.
     *
     * @param ClientInterface $client
     * @param LoggerInterface $logger
     */
    public function __construct(ClientInterface $client, LoggerInterface $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    /**
     * @inheritDoc
     */
    public function retrieve(string $url): string
    {
        try {
            $response = $this->client->request('GET', $url);

            return $response->getBody()->getContents();
        } catch (\Exception $exception) {
            $this->logger->error('Error: ' . $exception->getMessage());

            throw $exception;
        }
    }
}
<?php

namespace App\Infrastructure;

final class UriBuilder implements UriBuilderInterface
{
    /**
     * @inheritDoc
     */
    public function build(array $options): string
    {
        return sprintf('https://picsum.photos/v2/list?%s', http_build_query($options));
    }
}

See more on my official github repository https://github.com/oumarkonate/hexagonal-architecture

Open closed principle

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

When we define the responsibility of our class or our function, we have to implement it. Once our class or our function is validated, we must avoid to modifying them later to avoid regressions. All new features must be an extension of this class or function. By extension we talk about inheritance and polymorphism.

What does it mean concretely? ???

As example, consider some entities:

  • Circle represented by a class Circle.php
  • Octagon represented by a class Octagon.php
  • Rectangle represented by a class Rectangle.php
  • Star represented by a class Star.php

Consider the following class witch implements the decorator design pattern. Its objective is to decorate the entities.

<?php

namespace App\Presenter;

use App\Model\Circle;
use App\Model\Octagon;
use App\Model\Rectangle;
use App\Model\Star;

/**
 * Class Presenter
 */
final class Presenter implements PresenterInterface
{
    /** @var mixed */
    protected $model;

    /**
     * {@inheritDoc}
     */
    public function attach($model): void
    {
        $this->model = $model;
    }

    /**
     * {@inheritDoc}
     */
    public function getModel()
    {
        return $this->model;
    }

    /**
     * @inheritDoc
     */
    public function getTemplate(): string
    {
        if ($this->model instanceof Circle) {
            return 'objects/circle-template.html.twig';
        }

        if ($this->model instanceof Octagon) {
            return 'objects/octagon-template.html.twig';
        }

        if ($this->model instanceof Rectangle) {
            return 'objects/rectangle-template.html.twig';
        }

        if ($this->model instanceof Star) {
            return 'objects/star-template.html.twig';
        }

        return '';
    }
}
<?php

namespace App\Presenter;

/**
 * Interface ObjectInterface
 */
interface PresenterInterface
{
    /**
     * @param mixed $model
     */
    public function attach($model): void;

    /**
     * @return mixed
     */
    public function getModel();

    /**
     * @return string
     */
    public function getTemplate(): string;
}

Later, if we want to add a new entity Triangle then we will have to modify the method getTemplate(). And that violate the open closed principle.

By applying the open closed principle, the previous example becomes:

The Presenter class becomes abstract with abstract method abstract public function getTemplate()

<?php

namespace App\Presenter;

/**
 * Class AbstractPresenter
 */
abstract class AbstractPresenter implements PresenterInterface
{
    /** @var mixed */
    protected $model;

    /**
     * {@inheritDoc}
     */
    public function attach($model): void
    {
        $this->model = $model;
    }

    /**
     * {@inheritDoc}
     */
    public function getModel()
    {
        return $this->model;
    }

    /**
     * @return string
     */
    abstract public function getTemplate(): string;
}

Thus each class inheriting from AbstractPresenter must necessarily implement the method getTemplate()

<?php

namespace App\Presenter;

/**
 * Class CirclePresenter
 */
final class CirclePresenter extends AbstractPresenter
{
    /**
     * {@inheritDoc}
     */
    public function getTemplate(): string
    {
        return 'objects/circle-template.html.twig';
    }
}
<?php

namespace App\Presenter;

/**
 * Class OctagonPresenter
 */
final class OctagonPresenter extends AbstractPresenter
{
    /**
     * {@inheritDoc}
     */
    public function getTemplate(): string
    {
        return 'objects/octagon-template.html.twig';
    }
}
<?php

namespace App\Presenter;

/**
 * Class RectanglePresenter
 */
final class RectanglePresenter extends AbstractPresenter
{
    /**
     * {@inheritDoc}
     */
    public function getTemplate(): string
    {
        return 'objects/rectangle-template.html.twig';
    }
}
<?php

namespace App\Presenter;

/**
 * Class StarPresenter
 */
final class StarPresenter extends AbstractPresenter
{
    /**
     * {@inheritDoc}
     */
    public function getTemplate(): string
    {
        return 'objects/star-template.html.twig';
    }
}

Our starting problem is therefore resolved. We can add a new entity Triangle, it is enough to make a new presenter which inherits from AbstractPresenter

<?php

namespace App\Presenter;

/**
 * Class TrianglePresenter
 */
final class TrianglePresenter extends AbstractPresenter
{
    /**
     * {@inheritDoc}
     */
    public function getTemplate(): string
    {
        return 'objects/triangle-template.html.twig';
    }
}

We can add as many presenters as we want without modifying the class AbstractPresenter. As well as AbstractPresenter is closed to modification and new functionality will be done by extension.

See more on my official github repository: https://github.com/oumarkonate/design-patterns

Liskov substitution principle

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

What’s ??? ????

Don’t panic! that means

if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program

Let’s see a concrete example. Consider the example of Presenters:

Project link on my official github repository: https://github.com/oumarkonate/design-patterns.

The builder class of presenters collection like this:

<?php

namespace App\Builder;

use App\Factory\ObjectsFactoryInterface;
use App\Collection\ObjectsCollection;

/**
 * Class ObjectsCollectionBuilder
 */
class ObjectsCollectionBuilder implements ObjectsCollectionBuilderInterface
{
    /** @var iterable */
    private $models = [];

    /** @var ObjectsCollection */
    private $objectsCollection;

    /** @var ObjectsFactoryInterface */
    private $objectsFactory;

    /**
     * ObjectsCollectionBuilder constructor.
     *
     * @param ObjectsFactoryInterface $objectsFactory
     */
    public function __construct(ObjectsFactoryInterface $objectsFactory)
    {
        $this->objectsCollection = new ObjectsCollection();
        $this->objectsFactory = $objectsFactory;
    }

    /**
     * {@inheritDoc}
     */
    public function add($model): ObjectsCollectionBuilderInterface
    {
        $this->models[] = $model;

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function build(): ObjectsCollectionBuilderInterface
    {
        foreach ($this->models as $model) {
            $object = $this->objectsFactory->create($model);
            $object->attach($model);
            $this->objectsCollection->add($object);
        }

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function getObjectsCollection(): ObjectsCollection
    {
        return $this->objectsCollection;
    }
}

The presenters collection factory like this:

<?php

namespace App\Factory;

use App\Model\Circle;
use App\Presenter\PresenterInterface;
use Exception;
use App\Model\Rectangle;
use App\Model\Star;
use App\Model\Octagon;
use App\Presenter\CirclePresenter;
use App\Presenter\RectanglePresenter;
use App\Presenter\StarPresenter;
use App\Presenter\OctagonPresenter;

/**
 * Class ObjectsFactory
 */
class ObjectsFactory implements ObjectsFactoryInterface
{
    /**
     * {@inheritDoc}
     */
    public function create($model): PresenterInterface
    {
        if($model instanceof Circle) {
            return new CirclePresenter();
        }

        if($model instanceof Rectangle) {
            return new RectanglePresenter();
        }

        if($model instanceof Star) {
            return new StarPresenter();
        }

        if($model instanceof Octagon) {
            return new OctagonPresenter();
        }

        throw new Exception(sprintf('Object not found for model : %s', get_class($model)));
    }
}

The factory creates presenters which can be CirclePresenter, RectanglePresenter, OctagonPresenter, StarPresenter.

We are going to be interested to method build() in the builder class.

public function build(): ObjectsCollectionBuilderInterface
{
    foreach ($this->models as $model) {
        $object = $this->objectsFactory->create($model);
        $object->attach($model);
        $this->objectsCollection->add($object);
    }

    return $this;
}

We manipulate here an object Presenter without being interested in the type of presenter that it can be.
Whatever we manipulate an instance of type of Presenter, we need to be able to replace it with a different type of presenter.
Likewise we must be able to replace an instance of the parent Presenter with an instance of child Presenter… and the code should keep working.
Each child class (concrete presenter) have to manage it specificity and call to its specifics methods must remain within it class.

Interface segregation principle

No client should be forced to depend on methods it does not use.

It consists of

splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.

Consider the example of a class that manages live videos:

<?php

namespace App\Video;

final class Live implements VideoInterface
{

    public function getTitle(): string
    {
        // TODO: Implement getTitle() method.
    }

    public function getDescription(): string
    {
        // TODO: Implement getDescription() method.
    }

    public function getProgression(): int
    {
        // TODO: Implement getProgression() method.
    }
}
<?php

namespace App\Video;

interface VideoInterface
{
    public function getTitle(): string;

    public function getDescription(): string;

    public function getProgression(): int;
}

Imagine that we want to add a new class that manages replay videos. This class has only methods getTitle() and getDescription().

We will think about making a video class that will implement interface VideoInterface

This violates the interface segregation principle because the class video will be forced to implement the method getProgression() which it does not need.

To fix the problem, we have to create a new interface which contains the method getProgression().

<?php

namespace App\Video;

final class Video implements VideoInterface
{

    public function getTitle(): string
    {
        // TODO: Implement getTitle() method.
    }

    public function getDescription(): string
    {
        // TODO: Implement getDescription() method.
    }
}
<?php

namespace App\Video;

final class Live implements VideoInterface, ProgressionInterface
{

    public function getTitle(): string
    {
        // TODO: Implement getTitle() method.
    }

    public function getDescription(): string
    {
        // TODO: Implement getDescription() method.
    }

    public function getProgression(): int
    {
        // TODO: Implement getProgression() method.
    }
}
<?php

namespace App\Video;

interface VideoInterface
{
    public function getTitle(): string;

    public function getDescription(): string;
}
<?php

namespace App\Video;

interface ProgressionInterface
{
    public function getProgression(): int;
}

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).

Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

This means that a class should not depend on an instance of another class but on an abstraction. ???

Let’s see a concrete example. Consider the following class:

<?php

namespace App\Infrastructure;

use Monolog\Logger;
use \GuzzleHttp\Client;

/**
 * Class ApiClient
 */
final class ApiClient implements ApiClientInterface
{
    /**
     * @inheritDoc
     */
    public function retrieve(string $url): string
    {
        $logger = new Logger('my_text_logger');
        $client = new Client();

        try {
            $response = $client->request('GET', $url);

            return $response->getBody()->getContents();
        } catch (\Exception $exception) {
            $logger->error('Error: ' . $exception->getMessage());

            throw $exception;
        }
    }
}

The problem is that the Logger and the Client instances are frozen. If we decide to use an xml Logger or text Logger, it don’t work. Likewise if we decide to use another Client other than Guzzle, we will not able to do it… We will be forced to modify the method retrieve().

This is not good practice and violates the Dependency Inversion Principle.

The solution is to inject abstraction as dependencies.

<?php

namespace App\Infrastructure;

use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerInterface;

/**
 * Class ApiClient
 */
final class ApiClient implements ApiClientInterface
{
    /** @var ClientInterface */
   private $client;

   /** @var LoggerInterface */
   private $logger;

    /**
     * ApiClient constructor.
     *
     * @param ClientInterface $client
     * @param LoggerInterface $logger
     */
    public function __construct(ClientInterface $client, LoggerInterface $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    /**
     * @inheritDoc
     */
    public function retrieve(string $url): string
    {
        try {
            $response = $this->client->request('GET', $url);

            return $response->getBody()->getContents();
        } catch (\Exception $exception) {
            $this->logger->error('Error: ' . $exception->getMessage());

            throw $exception;
        }
    }
}

See more https://github.com/oumarkonate/hexagonal-architecture/blob/master/src/Infrastructure/ApiClient.php

The dependencies $client and $logger are abstraction.
The advantage is that we can instantiate our ApiClient with any Client that implements ClientInterface and any Logger that implements LoggerInterface.

$apiClient = new ApiClient(
    \GuzzleHttp\Client(), 
    Monolog\Logger('my_text_logger')
);

Or

$apiClient = new ApiClient(
    Psr\Http\Client\ClientInterface $client, 
    Psr\Log\LoggerInterface $logger
);

The dependency injection principle also facilitates units testing.

$builder = new ApiClient(
    $this->createMock(ClientInterface::class),
    $this->createMock(LoggerInterface::class)
);

The concept of dependency inversion is explained by the hexagonal architecture.

The dependence domain towards the infrastructure is reversed. See more http://oumarkonate.com/hexagonal-architecture-an-example-of-implementation/

Leave a Reply

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