Представим следующую ситуацию:
<?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)
oxidmod
11.04.2016 12:27+1у вас ошибочка в примере «Инъекция через конструктор»
в конструктор билдера new Page() вы так и не передали
Siddthartha
11.04.2016 14:24+1инъекция через конструктор логично выглядит. а вот зачем фабрика еще сверху? не совсем понимаю, выглядит как усложнение…
shiftedreality
11.04.2016 14:58Это как альтернатива, если инъекция через конструктор не предпочтительна
aivus
11.04.2016 14:441) Через set-методы можно сетить необязательные зависимости.
Еще удобно их использовать для иньекции зависимостей в случае наследования от какого-то класса с уже объявленным конструктором. Дабы не перегружать его. Насколько это правильно — вопрос, но нужно всегда иметь ввиду, что setter может быть не вызван. Всегда проверять значение переменной.
2) init-методы часто используются для того, чтобы сделать создание объекта как можно быстрее. А инициализация происходит в момент непосредственного использования. Эдакая lazy-загрузка. Пример
taliban
Это вот что было сейчас? «Не используйте переменные до их инициализации» на примере классов?
shiftedreality
Нет, статья о ненужных дополнительных инициализациях.