アーキテクチャの改善:依存関係の反転と注入、継承と構成

こんにちは。非常に多くの場合、古い(場合によってはそうではない)コードを操作するとき、またはある種のライブラリを使用しようとすると、拡張機能の制限に遭遇します。多くの場合、コードがアーキテクチャ的に読み書きできるものであれば問題はありません。最終的にコードの拡張、リファクタリング、および再利用を容易にする多くのアーキテクチャルールとパターンがあります。この記事では、例でそれらのいくつかに触れたいと思います。






昔、遠い遠いプロジェクトで、新しいパスワードを使ってユーザーにメールを送るサービスが登場しました。このようなもの:





<?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), , ),





一般-メッセージ本文、escapeHtmlメソッド





将軍を別々のクラスに入れてみましょう。





<?php

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

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



違いを見ると、一般に、両方のサービスはメッセージのテキストと受信者だけが異なります。両方のサービスを書き直して、互いに独立し、違いのみが含まれるようにします。





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



と元相続人





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



したがって、クラスは多くの依存関係を取得しましたが、テストでカバーしたり、コードの個々のセクションを再利用したりする方がはるかに便利になりました。それらの間のつながりを取り除き、それぞれのクラスを互いに独立して簡単に開発することができます。









PSもちろん、これらのクラスはまだ理想からは程遠いですが、それについてはまた別の機会に。








All Articles