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);
Возможно, такой подход будет не всегда оправдан, и такой уровень детализации излишен. Мы можем использовать любой из вариантов, но самое важное то, что именованные конструкторы дают нам возможность выбирать.
Автор: kriptomen