Как использовать именованные конструкторы в PHP

от автора

tl; dr — Не ограничивай себя одним конструктором в классе. Используй статические фабричные методы.

PHP позволяет использовать только один конструктор в классе, что довольно раздражительно. Вероятно, мы никогда не получим нормальную возможность перегрузки конструкторов в PHP, но кое-что сделать все же можно. Для примера возьмем простой класс, хранящий значение времени. Какой способ создания нового объекта лучше:

<?php $time = new Time("11:45"); $time = new Time(11, 45); 

Правильным ответом будет «в зависимости от ситуации». Оба способа могут являются корректным с точки зрения полученного результата. Реализуем поддержку обоих способов:

<?php final class Time {     private $hours, $minutes;     public function __construct($timeOrHours, $minutes = null)     {         if(is_string($timeOrHours) && is_null($minutes)) {             list($this->hours, $this->minutes) = explode($timeOrHours, ':', 2);         } else {             $this->hours = $timeOrHours;             $this->minutes = $minutes;         }     } } 

Выглядит отвратительно. Кроме того поддержка класса будет затруднена. Что произойдет, если нам понадобится добавить еще несколько способов создания экземпляров класса Time?

<?php $minutesSinceMidnight = 705; $time = new Time($minutesSinceMidnight); 

Также, вероятно, стоит добавить поддержку числовых строк (защита от дурака не помешает):

<?php $time = new Time("11", "45"); 

Реорганизация кода с использованием именованных конструкторов

Добавим несколько статичных методов для инициализации Time. Это позволит нам избавиться от условий в коде (что зачастую является хорошей идеей).

<?php final class Time {     private $hours, $minutes;      public function __construct($hours, $minutes)     {         $this->hours = (int) $hours;         $this->minutes = (int) $minutes;     }      public static function fromString($time)     {         list($hours, $minutes) = explode($time, ':', 2);         return new Time($hours, $minutes);     }      public static function fromMinutesSinceMidnight($minutesSinceMidnight)     {         $hours = floor($minutesSinceMidnight / 60);         $minutes = $minutesSinceMidnight % 60;         return new Time($hours, $minutes);     } } 

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

<?php final class Time {     private $time;      public function __construct($hours, $minutes)     {         $this->time = "$hours:$minutes";     }          public static function fromString($time)     {         list($hours, $minutes) = explode($time, ':', 2);         return new Time($hours, $minutes);     }     // ... } 

Это некрасиво: нам приходится разбивать строку, чтобы потом заново соединить её в конструкторе. А нужен ли нам конструктор для конструктора?

Мы встроили тебе конструктор в конструктор....

Нет, обойдемся без него. Реорганизуем работу методов, для работы с внутренним представлением напрямую, а конструктор сделаем приватным:

<?php final class Time {     private $hours, $minutes;      // Не удаляем пустой конструктор, т.к. это защитит нас от возможности создать объект извне     private function __construct(){}       public static function fromValues($hours, $minutes)     {         $time = new Time;         $time->hours = $hours;         $time->minutes = $minutes;         return $time;     }     // ... } 

Единообразие языковых конструкций

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

<?php $time1 = Time::fromValues($hours, $minutes); $time2 = Time::fromString($time); $time3 = Time::fromMinutesSinceMidnight($minutesSinceMidnight); 

Ничего не заметили? Именование методов не единообразно:

  • fromString — использует в названии детали реализации PHP;
  • fromValues ​​- использует своего рода общий термин программирования;
  • fromMinutesSinceMidnight — использует обозначения из предметной области.

Как языковой задрот гик, а также приверженец подхода Domain-Driven Design (Проблемо-ориентированное проектирование), я не мог пройти мимо этого. Т.к. класс Time является часть нашей предметной области, я предлагаю использовать для именования методов термины этой самой предметной области:

  • fromString => fromTime
  • fromValues => fromHoursAndMinutes

Такой акцент на предметной области дает нам широкий простор для действий:

<?php $customer = new Customer($name);  // В реальной жизни мы не используем такую терминологию // Мне  кажется, что так будет лучше: $customer = Customer::fromRegistration($name); $customer = Customer::fromImport($name); 

Возможно, такой подход будет не всегда оправдан, и такой уровень детализации излишен. Мы можем использовать любой из вариантов, но самое важное то, что именованные конструкторы дают нам возможность выбирать.

ссылка на оригинал статьи https://habrahabr.ru/post/279919/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *