Improving the Architecture: Dependency Inversion and Injection, Inheritance and Composition

Hello. Very often, when working with old (and sometimes not so) code, or trying to use some kind of library, you run into extension restrictions. Often there would be no problem if the code were architecturally literate. There are many architectural rules and patterns that ultimately make it easier to extend your code, refactor, and reuse. In this article I want to touch on some of them in examples.






A long time ago, in a distant distant project, a service appeared that sends an email with a new password to users. Something like this:





<?php

class ReminderPasswordService
{
    protected function sendToUser($user, $message)
    {
        $this->getMailer()->send([
            'from' => 'admin@example.com',
            'to' => $user['email'],
            'message' => $message
        ]);
    }

    public function sendReminderPassword($user, $password)
    {
        $message = $this->prepareMessage($user, $password);
        $this->sendToUser($user, $message);
    }

    protected function prepareMessage($user, $password)
    {
        $userName = $this->escapeHtml($user['first_name']);
        $password = $this->escapeHtml($password);
        $message = " {$userName}!
           {$password}";

        $message = $this->format($message);
        $message = $this->addHeaderAndFooter($message);

        return $message;
    }

    protected function format($message)
    {
        return nl2br($message);
    }

    protected function escapeHtml($string)
    {
        return htmlentities($string);
    }

    protected function addHeaderAndFooter($message)
    {
        $message = "<html><body>{$message}<br> , !</body>";

        return $message;
    }

    protected function getMailer()
    {
        return new Mailer('user', 'password', 'smtp.example.com');
    }

}
      
      



, .. , , , - . , , , , . - . plainText, HTML. ( , , ).





<?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService
{
    protected function send($user, $message)
    {
        $this->getMailer()->send([
            'from' => 'admin@example.com',
            'to' => 'manager@example.com',
            'message' => $message
        ]);
    }

    protected function prepareMessage($user, $password)
    {
        $userName = $this->escapeHtml($user['first_name']);
        $message = " {$userName}!
           ****";

        return $message;
    }

    protected function getMailer()
    {
        return new Mailer('user2', 'password2', 'smtp.corp.example.com');
    }
}
      
      



, , . smtp API . Mailer , . , , ?





Dependency Injection ( , DI)

DI - , , - , .





, . , , - . , - , . . Unit . , - DI, . :






<?php
class ReminderPasswordService
{
    /**
     * @var Mailer
     */
    protected $mailer;

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

    //   getMailer,   protected  $mailer

    // ...
}
      
      



, getMailer():





<?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService
{
    protected function send($to, $message)
    {
        $this->mailer->send([
            'from' => 'admin@example.com',
            'to' => 'manager@example.com',
            'message' => $message
        ]);
    }

    protected function prepareMessage($user, $password)
    {
        $userName = $this->escapeHtml($user['first_name']);
        $message = " {$userName}!
           ****";

        return $message;
    }
}
      
      



, , . , Mailer, ( , , ) . , , .





(Dependency Inversion Principle, DIP)

- , . - .





. , , , . : , .





<?php
interface MailerInterface
{
    public function send($emailFrom, $emailTo, $message);
}
      
      



.. - - MailMessageInterface , .





<?php
interface MailMessageInterface
{
    public function setFrom($from);
    public function getFrom();

    public function setTo($to);
    public function getTo();

    public function setMessage($message);
    public function getMessage();
}
      
      



MailSenderInterface, ,





<?php
interface MailerInterface
{
    public function send(MailMessageInterface $message);
}
      
      



- MailMessageInterface,





<?php
interface MailMessageFactoryInterface
{
    public function create(): MailMessageInterface;
}
      
      



, ,





<?php
class ReminderPasswordService
{
    /**
     * @var MailerInterface
     */
    protected $mailer;

    /**
     * @var MailMessageFactoryInterface
     */
    protected $messageFactory;

    public function __construct(MailerInterface $mailer, MailMessageFactoryInterface $messageFactory)
    {
        $this->mailer = $mailer;
        $this->messageFactory = $messageFactory;
    }

    protected function send($user, $messageText)
    {
        $message = $this->messageFactory->create();
        $message->setFrom('admin@example.com');
        $message->setTo($user['email']);
        $message->setMessage($messageText);

        $this->mailer->send($message);
    }

    //    

    public function sendReminderPassword($user, $password)
    {
        $message = $this->prepareMessage($user, $password);
        $this->sendToUser($user, $message);
    }

    protected function prepareMessage($user, $password)
    {
        $userName = $this->escapeHtml($user['first_name']);
        $password = $this->escapeHtml($password);
        $message = " {$userName}!
           {$password}";

        $message = $this->format($message);
        $message = $this->addHeaderAndFooter($message);

        return $message;
    }

    protected function format($message)
    {
        return nl2br($message);
    }

    protected function escapeHtml($string)
    {
        return htmlentities($string);
    }

    protected function addHeaderAndFooter($message)
    {
        $message = "<html><body>{$message}<br> , !</body>";

        return $message;
    }
}
      
      



, , . .





<?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService
{
    protected function send($to, $messageText)
    {
        $message = $this->messageFactory->create();
        $message->setFrom('admin@example.com');
        $message->setTo('manager@example.com');
        $message->setMessage($messageText);

        $this->mailer->send($message);
    }

    protected function prepareMessage($user, $password)
    {
        $userName = $this->escapeHtml($user['first_name']);
        $message = " {$userName}!
           ****";

        return $message;
    }
}
      
      



VS

- . - , .





:





1. , .





2. , protected/private





3. , - - .





, , - , , . 90% ( , , ), .





, . , API, -





<?php
class SomeAPIService implements SomeAPIServiceInterface
{
    public function getSomeData($someParam)
    {
        $someData = [];
        // ...
        return $someData;
    }
}
      
      



, , . :





<?php
class SomeApiServiceCached extends SomeAPIService
{
    public function getSomeData($someParam)
    {
        $cachedData = $this->getCachedData($someParam);
        if ($cachedData === null) {
            $cachedData = parent::getSomeData($someParam);
            $this->saveToCache($someParam, $cachedData);
        }

        return $cachedData;
    }

    // ...
}
      
      



API , , DIP, .





<?php

class SomeApiServiceCached implements SomeAPIServiceInterface
{
   private $someApiService;

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

    public function getSomeData($someParam)
    {
        $cachedData = $this->getCachedData($someParam);
        if ($cachedData === null) {
            $cachedData = $this->someApiService->getSomeData($someParam);
            $this->saveToCache($someParam, $cachedData);
        }

        return $cachedData;
    }

    // ...
}
      
      



, , .





ReminderPasswordCopyToManagerService , " ". , - addHeaderAndFooter format, prepareMessage ( - (Open-Closed Principe), , ),





General - message body, escapeHtml method .





Let's try to bring the general into separate classes.





<?php

class ReminderPasswordMessageTextBuilder
{
    public function buildMessageText($userName, $password)
    {
        return " {$userName}!
           {$password}";
    }
}

class Escaper
{
    public function escapeHtml($string)
    {
        return htmlentities($string);
    }
}
      
      



If we look at the differences, then in general both services differ only in the text of the message, as well as in the recipients. Let's rewrite both services so that they are independent from each other and contain only differences.





<?php
class ReminderPasswordService
{
    //  ,    
    private $mailer;
    private $messageFactory;
    private $escaper;
    private $messageTextBuilder;

    public function __construct(
        MailerInterface $mailer,
        MailMessageFactoryInterface $messageFactory,
        Escaper $escaper,
        ReminderPasswordMessageTextBuilder $messageTextBuilder
    ) {
        $this->mailer = $mailer;
        $this->messageFactory = $messageFactory;
        $this->escaper = $escaper;
        $this->messageTextBuilder = $messageTextBuilder;
    }

    public function sendReminderPassword($user, $password)
    {
        $messageText = $this->prepareMessage($user, $password);

        $message = $this->messageFactory->create();
        $message->setFrom('admin@example.com');
        $message->setTo($user['email']);
        $message->setMessage($messageText);

        $this->mailer->send($message);
    }

    private function prepareMessage($user, $password)
    {
        $userName = $this->escaper->escapeHtml($user['first_name']);
        $password = $this->escaper->escapeHtml($password);
        $message = $this->messageTextBuilder->buildMessageText($userName, $password);
        $message = $this->format($message);
        $message = $this->addHeaderAndFooter($message);

        return $message;
    }

    //        .
    private function addHeaderAndFooter($message)
    {
        $message = "<html><body>{$message}<br> , !</body>";

        return $message;
    }

    private function format($message)
    {
        return nl2br($message);
    }
}
      
      



and former heir





<?php
class ReminderPasswordCopyToManagerService
{
    private $mailer;
    private $messageFactory;
    private $escaper;
    private $messageTextBuilder;

    public function __construct(
        MailerInterface $mailer,
        MailMessageFactoryInterface $messageFactory,
        Escaper $escaper,
        ReminderPasswordMessageTextBuilder $messageTextBuilder
    ) {
        $this->mailer = $mailer;
        $this->messageFactory = $messageFactory;
        $this->escaper = $escaper;
        $this->messageTextBuilder = $messageTextBuilder;
    }

    public function sendReminderPasswordCopyToManager($user)
    {
        $messageText = $this->prepareMessage($user);

        $message = $this->messageFactory->create();
        $message->setFrom('admin@example.com');
        $message->setTo($user['email']);
        $message->setMessage($messageText);

        $this->mailer->send($message);
    }

    private function prepareMessage($user)
    {
        $userName = $this->escaper->escapeHtml($user['first_name']);
        $message = $this->messageTextBuilder->buildMessageText($userName, '****');

        return $message;
    }
}
      
      



Thus, although the classes have acquired a number of dependencies, it has become much more convenient to cover with tests or reuse individual code sections. We got rid of the connection between them, and we can easily develop each separate class independently of the other.









PS, of course, these classes are still far from ideal, but more on that another time.








All Articles