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/
Добавить комментарий