tl; dr — Не ограничивай себя одним конструктором в классе. Используй статические фабричные методы.
PHP позволяет использовать только один конструктор в классе, что довольно раздражительно. Вероятно, мы никогда не получим нормальную возможность перегрузки конструкторов в PHP, но кое-что сделать все же можно. Для примера возьмем простой класс, хранящий значение времени. Какой способ создания нового объекта лучше:
Правильным ответом будет «в зависимости от ситуации». Оба способа могут являются корректным с точки зрения полученного результата. Реализуем поддержку обоих способов:
Выглядит отвратительно. Кроме того поддержка класса будет затруднена. Что произойдет, если нам понадобится добавить еще несколько способов создания экземпляров класса Time?
Также, вероятно, стоит добавить поддержку числовых строк (защита от дурака не помешает):
Добавим несколько статичных методов для инициализации Time. Это позволит нам избавиться от условий в коде (что зачастую является хорошей идеей).
Теперь каждый метод удовлетворяет принцип Единой ответственности. Публичный интерфейс прост и понятен. Вроде бы закончили? Меня по прежнему беспокоит конструктор, он использует внутреннее представление объекта, что затрудняет изменение интерфейса. Положим, по какой-то причине нам необходимо хранить объединенное значение времени в строковом формате, а не по отдельности, как раньше:
Это некрасиво: нам приходится разбивать строку, чтобы потом заново соединить её в конструкторе. А нужен ли нам конструктор для конструктора?
Нет, обойдемся без него. Реорганизуем работу методов, для работы с внутренним представлением напрямую, а конструктор сделаем приватным:
Наш код стал чище, мы обзавелись несколькими полезными методами инициализации нового объекта. Но как часто случается с хорошими конструктивными решениями — ранее скрытые изъяны выбираются на поверхность. Взгляните на пример использования наших методов:
Ничего не заметили? Именование методов не единообразно:
Как языковойзадрот гик, а также приверженец подхода Domain-Driven Design (Проблемо-ориентированное проектирование), я не мог пройти мимо этого. Т.к. класс Time является часть нашей предметной области, я предлагаю использовать для именования методов термины этой самой предметной области:
Такой акцент на предметной области дает нам широкий простор для действий:
Возможно, такой подход будет не всегда оправдан, и такой уровень детализации излишен. Мы можем использовать любой из вариантов, но самое важное то, что именованные конструкторы дают нам возможность выбирать.
Часть 2: Когда использовать статические методы
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 - использует обозначения из предметной области.
Как языковой
- fromString => fromTime
- fromValues => fromHoursAndMinutes
Такой акцент на предметной области дает нам широкий простор для действий:
<?php
$customer = new Customer($name);
// В реальной жизни мы не используем такую терминологию
// Мне кажется, что так будет лучше:
$customer = Customer::fromRegistration($name);
$customer = Customer::fromImport($name);
Возможно, такой подход будет не всегда оправдан, и такой уровень детализации излишен. Мы можем использовать любой из вариантов, но самое важное то, что именованные конструкторы дают нам возможность выбирать.
Часть 2: Когда использовать статические методы