В приложении с объектно-ориентированной архитектурой часто встает вопрос: кто должен отвечать за создание того или иного объекта? Многие современные решения используют Локатор служб для инстанциации и хранения объектов инфраструктуры и даже некоторых элементов бизнес-модели, но этого часто не достаточно. Рассмотрим простой пример:

<?php
class Message{
  // @var string Заголовок сообщения.

  private $title;

  //  @var string Имя шаблона сообщения.

  private $template;

  // Getters и Setters

  ...
}

class MailMessage extends Message{
  // @var string Отправитель сообщения.

  private $from;

  // @var string Получатель сообщения.

  private $to;

  // Отправляет сообщение получателю.
  
  public function send(){
    // Реализация

    ... 
  }
}

Предполагается, что использоваться эти классы будут в методах Order::pay (оплата заказа), Auth::registerNewUser (регистрация нового пользователя) и Article::create (создание новой статьи).

Какая часть системы должна отвечать за создание экземпляров класса MailMessage и следует ли вызывать метод send сразу после инстанциации?

Фабричный метод

Паттерн Фабричный метод достаточно прост в реализации и использовании. Он имеет следующую структуру:

Структура паттерна Фабричный метод

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

Чем же фабричный метод лучше обычного конструктора? Тем что он позволяет сформировать конфигурацию (параметры) конструктора перед его вызовом. Так, для создания соединения с БД необходимо прежде получить конфигурацию для этого соединения, и фабричный метод позволит собрать всю логику инстанциации в одном месте:

<?php
class Db{
  // Статичный фабричный метод

  public static function getInstance(){
    $config = include('config/database.php');
    $dsn = sprintf(
      '%s:host=%s;dbname=%s',
      isset($config['driver'])? $config['driver'] : 'mysql',
      isset($config['host'])? $config['host'] : 'localhost',
      $config['dbname']
    );

    return new PDO($dsn, $config['login'], $config['password']);
  }
}

Как видно из примера, фабричный метод может быть статичным. Часто это используется для инстанциации вызываемого класса. Не статичные методы больше применяются для создания экземпляров других классов.

Вернемся к нашей задаче из вводной. Где разместить фабричный метод, который будет создавать экземпляры класса MailMessage? Учитывая что для рассылки писем необходим адресат (свойство $to), логичным решением будет разместить его в классе User с именем notifyFromEmail. Метод будет создавать сообщение для вызываемого пользователя:

<?php
class User{
  // Реализация

  ...

  public function notifyFromEmail($title, $template, $from = null){
    $message = new MailMessage;
    $message->setTitle($title);
    $message->setTemplate($template);
    // Заполнение адресата email текущего пользователя

    $message->setTo($this->getEmail());
    if(!is_null($from)){
      $message->setFrom($from);
    }

    return $message;
  }
}

// Использование

$message = $currentUser->notifyFromEmail('Заголовок', 'email/notifi.tpl');
$message->send();

Теперь имея ссылку на пользователя мы можем легко оповестить его с помощью письма об интересных событиях:

<?php
class Auth{
  public static function registerNewUser($login, $password){
    $user = new User;
    $user->setEmail($login);
    $user->setPassword($password);

    // Оповещение

    $user->notifyFromEmail('Вы зарегистрированы', 'email/new_account.tpl', 'system@site.com')
      ->send();

    return $user;
  }
}

Работа с объектом в фабричном методе

Из предыдущих примеров у вас может возникнуть желание вызывать метод send прямо в фабричном методе notifyFromEmail. Не делайте так! Запомните - фабричный метод должен только создавать и конфигурировать объекты, а не взаимодействовать с ними. За сохранение созданного объекта в базе данных или передачу созданного сообщения по электронной почте должен отвечать объект, вызвавший фабричный метод или инфраструктура системы. Запомнили? Хорошо. Теперь попробуем понять почему так.

Дело в том, что взаимодействие с объектом внутри фабричного метода делает решение менее гибким. Рассмотрим пример:

<?php
class Auth{
  public static function registerNewUser($login, $password){
    $user = new User;
    $user->setEmail($login);
    $user->setPassword($password);

    // Оповещение

    $user->notifyFromEmail('Вы зарегистрированы', 'email/new_account.tpl', 'system@site.com')
      ->send();

    return $user;
  }
}

После вызова registerNewUser предполагается, что регистрируемый пользователь будет оповещен с помощью email-рассылки и появляется желание добавить эту логику прямо в метод. Но что делать, если мы не должны рассылать уведомления пользователям, зарегистрированным в системе посредством выгрузки из CSV файла? В этом случае нам придется повторить логику фабричного метода registerNewUser без оповещения. В данном случае проще будет отказаться от взаимодействия с создаваемым внутри метода объектом:

<?php
class Auth{
  public static function registerNewUser($login, $password){
    $user = new User;
    $user->setEmail($login);
    $user->setPassword($password);

    return $user;
  }
}

class UserController{
  // Обычная регистрация пользователя с оповещением

  public function registerAction($email, $password){
    $user = Auth::registerNewUser($email, $password);

    // Оповещение

    $user->notifyFromEmail('Вы зарегистрированы', 'email/new_account.tpl', 'system@site.com')
      ->send();

    $db->save($user);
  }

  // Выгрузка пользователей из CSV файла без оповещения

  public function exportUsersAction($csvFile){
    ...

    while(...){
      $user = Auth::registerNewUser($data['email'], $data['password']);
      $db->save($user);
    }
  }
}

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