Про архитектуру приложений на PHP было написано не один десяток статей, но на данной проблеме больше акцентируют внимание разработчики Java и C#. Суть ее заключается в жесткой зависимости одного свойства на другом.

Представим следующую ситуацию:

Использование сеттера
<?php

class Page
{
    /**
     * @var string
     */
    private $content;

    /**
     * @param $content
     */
    public function setContent($content)
    {
        $this->content = $content;
    }
}

class PageBuilder
{
    /**
     * @var Page
     */
    private $page;

    /**
     * @param $page
     */
    public function setPage($page)
    {
        $this->page = $page;
    }

    /**
     * @param $content
     * @return $this
     */
    public function setContent($content)
    {
        $this->page->setContent($content);

        return $this;
    }

    /**
     * @return Page
     */
    public function build()
    {
        return $this->page;
    }
}


$pageBuilder = new PageBuilder();
$pageBuilder->setPage(new Page());
$pageBuilder->setContent('Test content');
$pageBuilder->build();

В данном примере видно, что $pageBuilder->build() является потенциально опасным и может привести к фатальной ошибке если $pageBuilder->setPage(new Page()) не был предварительно вызван. Другая часто встречающаяся ошибка — использование методов init() или initialization():

Использование инициализатора
class Page
{
    // Class
}

class PageBuilder
{
    /**
     * @var Page
     */
    private $page;

    /**
     * Initialization
     */
    public function init()
    {
        $this->page = new Page();
    }

    /**
     * @param $content
     * @return $this
     */
    public function setContent($content)
    {
        $this->page->setContent($content);

        return $this;
    }

    /**
     * @return Page
     */
    public function build()
    {
        return $this->page;
    }
}


$pageBuilder = new PageBuilder();
$pageBuilder->init();
$pageBuilder->setContent('Test content');
$pageBuilder->build();

Если мы забудем вызвать метод init(), нас также ждут неприятности. Данный код является отличным примером плохой архитектуры приложения. Методы-инициализаторы пытаются вести себя как конструкторы, которыми не являются по определению.

Для избежания Temporal Coupling нужно всегда пользоваться правилами:

  • экземпляр класса должен быть готов для использования сразу же после создания;
  • конструкторы не должны выполнять никакой логики кроме инициализации свойств класса;

Инъекция зависимости через конструктор


Это решение является оптимальным и предпочтительным в большинстве случаев. Мы можем использовать механизмы Dependency Injection из Symfony, Laravel или других современных фреймворков.

Инъекция через конструктор
class Page
{
    // Class
}

class PageBuilder
{
    /**
     * @var Page
     */
    private $page;

    /**
     * PageBuilder constructor.
     * @param Page $page
     */
    public function __construct(Page $page)
    {
        $this->page = $page;
    }

   // Методы-сеттеры
}

$pageBuilder = new PageBuilder(new Page());
$pageBuilder->setContent('Test content');
$pageBuilder->build();


Абстрактная фабрика


Немного модифицируем наш код, добавив абстрактную фабрику:

Абстрактная фабрика
<?php

class Page
{
    // Class
}

class PageBuilder
{
    /**
     * @var Page
     */
    private $page;

    /**
     * PageBuilder constructor.
     * @param Page $page
     */
    public function __construct(Page $page)
    {
        $this->page = $page;
    }

    /**
     * @param $content
     * @return $this
     */
    public function setContent($content)
    {
        $this->page->setContent($content);

        return $this;
    }

    /**
     * @return Page
     */
    public function build()
    {
        return $this->page;
    }
}

class PageBuilderFactory implements FactoryInterface
{
    /**
     * @param Page|null $page
     * @return PageBuilder
     */
    public function create(Page $page = null)
    {
        if (null === $page) {
            $page = new Page();
        }

        return new PageBuilder($page);
    }
}

$pageBuilderFactory = new PageBuilderFactory();
$pageBuilder = $pageBuilderFactory->create();
$pageBuilder->setContent('Test content');
$pageBuilder->build();


Как видим, экземпляр класса Page создан без явного вызова и будет доступен нашему билдеру.

Заключение


Temporal Coupling нужно всегда избегать, вне зависимости от сложности приложения, влияния code-review или других факторов. Также помните что конструкторы должны выполнять только логику, связанную с инъекциями. Иначе вы рискуете получить ухудшение быстродействия на этапе создания экземпляров класса.

Полезные ссылки


Комментарии (11)


  1. taliban
    11.04.2016 12:19
    +3

    Это вот что было сейчас? «Не используйте переменные до их инициализации» на примере классов?


    1. shiftedreality
      11.04.2016 13:17
      +2

      Нет, статья о ненужных дополнительных инициализациях.


  1. oxidmod
    11.04.2016 12:27
    +1

    у вас ошибочка в примере «Инъекция через конструктор»
    в конструктор билдера new Page() вы так и не передали


    1. shiftedreality
      11.04.2016 12:28
      +1

      Спасибо, подправил


  1. dbelka
    11.04.2016 12:39

    del


  1. Siddthartha
    11.04.2016 14:24
    +1

    инъекция через конструктор логично выглядит. а вот зачем фабрика еще сверху? не совсем понимаю, выглядит как усложнение…


    1. shiftedreality
      11.04.2016 14:58

      Это как альтернатива, если инъекция через конструктор не предпочтительна


  1. aivus
    11.04.2016 14:44

    1) Через set-методы можно сетить необязательные зависимости.
    Еще удобно их использовать для иньекции зависимостей в случае наследования от какого-то класса с уже объявленным конструктором. Дабы не перегружать его. Насколько это правильно — вопрос, но нужно всегда иметь ввиду, что setter может быть не вызван. Всегда проверять значение переменной.

    2) init-методы часто используются для того, чтобы сделать создание объекта как можно быстрее. А инициализация происходит в момент непосредственного использования. Эдакая lazy-загрузка. Пример