Pull to refresh

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

Reading time 3 min
Views 23K
Original author: Mathias Verraes
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);

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


Часть 2: Когда использовать статические методы
Tags:
Hubs:
+15
Comments 97
Comments Comments 97

Articles