Mejora de la arquitectura: inversión e inyección de dependencias, herencia y composición

Hola. Muy a menudo, cuando se trabaja con código antiguo (ya veces no es así) o se intenta utilizar algún tipo de biblioteca, se encuentran restricciones de extensión. A menudo, no habría ningún problema si el código tuviera conocimientos de arquitectura. Hay muchas reglas y patrones arquitectónicos que, en última instancia, facilitan la extensión, refactorización y reutilización del código. En este artículo quiero mencionar algunos de ellos en ejemplos.






Hace mucho tiempo, en un lejano proyecto lejano, apareció un servicio que envía una carta con una nueva contraseña a los usuarios. Algo como esto:





<?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: cuerpo del mensaje, método escapeHtml .





Intentemos llevar el general a clases separadas.





<?php

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

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



Si observamos las diferencias, en general ambos servicios difieren solo en el texto del mensaje, así como en los destinatarios. Reescribamos ambos servicios para que sean independientes entre sí y solo contengan diferencias.





<?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);
    }
}
      
      



y ex heredero





<?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;
    }
}
      
      



Por lo tanto, aunque las clases han adquirido una serie de dependencias, se ha vuelto notablemente más conveniente cubrirlas con pruebas o reutilizar secciones individuales del código. Nos deshicimos de la conexión entre ellos y podemos desarrollar fácilmente cada clase por separado independientemente de la otra.









PD: Por supuesto, estas clases todavía están lejos de ser ideales, pero hablaremos de eso en otro momento.








All Articles