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

Original author: Mathias Verraes
  • Translation
  • Tutorial
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: Когда использовать статические методы
Share post

Comments 97

    –9
    А как этот подход уживается с Dependency Injection (Zend, Symfony, ...)?
      +4
      Как заметил oxidmod, если у нас есть более высокий уровень, которому можно делегировать вопрос создание объектов, то текущий подход не очень-то и применим.

      Тут больше раскрывается вопрос инкапсуляции логики создания объекта.
        +5
        Автор ( и я его поддерживаю) предлагает использовать именованные конструкторы ( в частной реализации PHP это статические фабричные методы) для объектов доменной области, а в них DI не нужен.
        А если уж очень нужно иметь сервис с разными конструкторами ( сложно представить такую ситуацию), то в том же Symfony DI есть поддержка фабрик.
          0
          Меня интересовало, что будет инжектиться в класс, завязанный на класс Time, если его конструктор недоступен:

          class DependedClass {
          
              public function __construct(Time $time) {
                  // ...
              }
          }

          Т.е., данный подход предполагает в таких случаях создавать фабрики, которые будут создавать объекты с использованием их собственных статических фабричных методов, и уже эти фабрики инжектить в зависящие от объектов классы (или, как в случае с Symfony, указывать в настройках DI, что для создания экземпляров Time нужно использовать "factory: [TimeFactory, create]"):

          class TimeFactory {
              public function create() {
                  $result = Time::fromValues(0, 0);
                  return $result;
              }
          }
          
          class DependedClass {
          
              public function __construct(TimeFactory $factory) {
                  // ...
              }
          }

          Вполне возможно, в этом есть какой-то сакральный смысл, но я бы все-таки конструктор не прятал. Ну или очень сильно ограничил бы применение "именованных конструкторов" — все-таки статика к тестам совсем не friendly. Вообщем, мне эти "именованные конструкторы" как-то не совсем по душе. Но смотрятся красиво, не отрицаю.
            0
            Прошел по ссылке на оригинальную статью. Насколько я понял в "вопросах-ответах" Матиас считает, что этот подход хорош в самых простых объектах, которые и тестировать-то нет неообходимости. Ну что ж, каждое решение имеет свою область применения.
              0
              которые и тестировать-то нет неообходимости

              Вы не правильно поняли Матиаса. Тестировать их надо, и с этим проблем нет. А вот мокать их не нужно. И уж тем более у вас не должно быть классов, которые требуют тот же Time в конструкторе (ну разве что это конструктор такого-же объекта-значения или сущности).

              Time — это объект представляющий состояние нашей системы. Данные приложения. Мокать же мы должны только сервисы. Причем не просто сервисы, а какие-то интерфейсы, имплементация которых не входит в рамки конкретного модуля (принцип инверсии зависимостей в действии).
                +2
                Все же на всякий случай еще раз поясню. Статические методы-фабрики нужны для объектов значений. Эти объекты — это по сути то же самое что строки, числа и т.д. просто чуть более жирные. Но с точки зрения приложения это просто данные.
                Сущности (User например) — это объекты-значения у которых есть идентификатор. Ну то есть опять же это объекты скрывающие в себе состояние системы, а стало быть их не нужно мокать.

                Вообще как думаете, может стоит отдельно статью написать по поводу того, как писать поддерживаемые тесты? Ну мол, "не мокать что попало просто потому что можно", "не завязывать тесты на реализацию", "тестировать только публичные интерфейсы".

                  0
                  Я бы с удовольствием прочел, пишите!
                    0
                    Обязательно пишите!
                      +1
                      Спасибо за пояснение. Насколько я понял, предлагается делать "фабричные методы" для "POJO like" классов ("объектов значений" по вашей терминологии) в самих классах, а не выносить фабричные методы в отдельные фабрики. Такой подход не применим к классам, имплементирующим преобразование данных и используемым другими классами ("сервисов" по вашей терминологии). В случае необходимости использовать в конструкторе какого-либо класса "объектов значений" нужно не создавать фабрику этих "объектов значений", а пересмотреть необходимость использования "объектов значений" в конструкторе класса или переквалифицировать "объект значение" в другую роль и создать ему публичный конструктор.

                      Границы применимости предложенного Матиасом решения для меня стали довольно очевидны. Спасибо еще раз.
                        +1
                        («объектов значений» по вашей терминологии)

                        Это нормальная общепринятая терминология. Ищите Value Objects.
                          –1
                            0
                            Красивая картинка. Но к чему вы ее привели?
                              0
                              Там чуть выше картинке есть ссылка на статью, где объясняется сама картинка. А привел я ее к тому, что вы решили уточнить, что именно значит "объектов значений". Для наглядности, что я имел в виду под "POJO like" классами (в данной картинке они проходят под именем POCO, т.к. статья дотнетовская). Раз уж совмещать используемые термины, то наглядно.
                                0
                                Лично мне не нравится эта диаграмма. Value Object ни разу не является plain. Инкапсуляция Value Object'ом части доменной логики есть принципиальное его качество.
                                В общем, имхо Вы еще больше все запутали.
                                Value Object'ы — это атрибуты. То, значением чего Entity отличаются друг от друга.
                                  +1
                                  plain в данном случае не означает отсутствие логики. Имеется в виду незавязанность на какие-то внешние интерфейсы. Максимально простой класс без зависимостей. https://en.wikipedia.org/wiki/Plain_Old_Java_Object
                                    0
                                    Ничего страшного. Вы можете считать, как вам удобнее. Я не настаиваю на том, что диаграмма верна. Просто она коррелирует с моим представлением о прекрасном, и поэтому она здесь.
                                      0
                                      Вы путаете plain object с anemic model.
                        +1
                        Меня интересовало, что будет инжектиться в класс, завязанный на класс Time, если его конструктор недоступен:

                        М? Вы же инжектите уже готовый объект. В чем проблема?
                          –1
                          В его создании DI-фреймворком, "если конструктор недоступен".
                            +1
                            Time — это объект-значение. Он не имеет какого-то сложного поведения, которое можно было бы использовать — вся его ценность в тех данных, которыми он наполнен.

                            В чем вообще смысл создания таких объектов DI-фреймворком автоматически? Представьте, что там не Time, а integer. Если ли смысл создавать integer автоматически?
                              +2
                              Он не имеет какого-то сложного поведения, которое можно было бы использовать — вся его ценность в тех данных, которыми он наполнен.

                              А вот это не правда. Он вполне себе может внутри делать какие-то мега сложные вещи и его вполне себе нужно тестировать. Но мокать его не нужно, поскольку если мы его юзаем в юнит тестах, то это уже часть тестируемого модуля. Мокают обычно то, что может иметь много реализаций (репозитории, менеджеры файлов и т.д.), что бы не завязывать тесты на конкретную реализацию (более того реализации может и не быть, мы можем просто объявить интерфейс, инверсия зависимостй и все дела).

                              В чем вообще смысл создания таких объектов DI-фреймворком автоматически?

                              Потому что когда люди начинают пользоваться DI они начинают делать все через него. Оверюз вещей — больное место всех разработчиков.
                                0
                                Если сервис Х имеет зависимость от обьекта Time, то описание Х в DI подразумевает передачу экземпляра Time, а значит придётся описать создание Time, что делается через фабрики. В таком подходе теряется смысл статических конструкторов.
                                  +1
                                  Если сервис Х имеет зависимость от обьекта Time

                                  Если у вас такая ситуация — значит у вас какой-то странный сервис. Time будет аргументом метода сервиса скорее, а не зависимостью.
                                  Time — это то же самое что string или int. Какое-то "значение".
                                    0
                                    Ttl к кэш сервису может задаваться в конструкторе как раз этим Time. На мой взгляд вполне реальный момент.
                                      0
                                      Ну это уже не "сервисы" а "параметры", чуть по другому оно делается. Ну и опять же — скорее всего там будет значение в виде скаляра, которое будет прокидываться уже в метод-фабрику объекта Time. Ну мол для упрощения интерфейса, как никак объект Time нам в этом ключе только внутри нужен.
                                        0
                                        Ну зачем так сложно-то? Зачем вообще фабрика? DI-фреймворки на PHP что, не умеют константные значения для параметров принимать?
                                          0
                                          Нет, я имею в виду такое:
                                          public function __construct(Foo $foo, Bar $bar, int $defaultTTL) {
                                              // ...
                                              $this->defaultTTL = Time::create($defaultTTL);
                                          }

                                          Что передаваться будет просто константное значение, а оборачиваться в объект оно будет внутри. Как никак этот самый Time предполагается использовать только внутри а стало быть внешнему миру лучше о нем вообще ничего не знать.
                                            0
                                            В таком случае теряются преимущества нескольких конструкторов, ведь фактически будет использоваться лишь один.
                                +3
                                Моим заблуждением было, что я читаю статью, посвященную применению вместо конструкторов с параметрами статических фабричных методов при создании различных объектов. Как оказалось, статья относится только к конструированию "объектов-значений".

                                IMHO, эпиграф было бы лучше поставить такой:

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

                                Вполне возможно, в таком случае у меня бы не возникло вопроса, как инжектить объект класса Time (или integer, как вы резонно заметили).
                                0
                                За других не скажу, а у симфонии с этим проблем на моей памяти не возникало.
                                  0
                                  Проверил.
                                  use Symfony\Component\DependencyInjection\ContainerBuilder;
                                  
                                  $container = new ContainerBuilder();
                                  $container->register('time', Time::class);
                                  $obj = $container->get('time');

                                  Вылетает исключение:
                                  PHP Fatal error:  Uncaught exception 'ReflectionException' with message 'Access to non-public constructor of class ...\Time

                                  Все из-за этого:
                                      // Не удаляем пустой конструктор, т.к. это защитит нас от возможности создать объект извне
                                      private function __construct()
                                      {
                                      }
                                    –1
                                    Мельком взглянул исходники, должно вот так завестись:
                                    $container = new ContainerBuilder();
                                    $container->setDefinition('time', (new Definition())->setFactory('Time::create'));
                                    
                                    $obj = $container->get('time');

                                    Просьба симфонистов поправить, если что не так.
                                      0
                                      Да, спасибо, так работает.
                                      use Symfony\Component\DependencyInjection\ContainerBuilder;
                                      use Symfony\Component\DependencyInjection\Definition;
                                      
                                      $container = new ContainerBuilder();
                                      // $container->register('time', Time::class);
                                      $container->setDefinition('time', (new Definition())->setFactory(Time::class . '::fromValues'));
                                      // $obj = Time::fromValues(2, 3);
                                      $obj = $container->get('time');

                                      Только warning вылетает, но это уже мелочи на общем фоне :)
                                      PHP Warning:  Missing argument 1 for ...\Time::fromValues()

                                      Если дожать еще передачу в DI-фабрику default-параметров для "именованного конструктора", то можно будет снимать свой вопрос по поводу использования в DI-фреймворках объектов с приватным конструктором.
                                        +1
                                        фабрике можно аргументы задавать. То есть если сильно извращаться — можно крутить как хочешь. Вот только я бы по рукам бил за желание пихать такие объекты в конструктор сервисов.
                        +2
                        мне кажется, что вопрос правильного создания модели не должен лежать в зоне отвественности этой самой модели. Да и обращение к приватным полям объекта извне этого объекта является больше хаком и не факт что так будет работать всегда. ИМХО, стоит иметь нормальный конструктор, с четко определенными параметрами. А вопрос создания объекта делегировать на более высокий уровень.
                        Допустим у Вас в системе могут появлятся пользователи из формы регистрации на вашем сайте, а могут быть импортированы из системы партнера.
                        Вы можете создать класс-менеджер, который будет уметь привести данные с формы/с csv/с запроса на апи в общий вид и создать объект пользователя. так вы сможете в любой момент добавить новый способ не трогая доменную модель
                          +2
                          Да и обращение к приватным полям объекта извне этого объекта является больше хаком и не факт что так будет работать всегда.

                          А где в предложенном подходе обращение к приватным полям объекта извне? Все происходит внутри самого объекта и полностью согласуется с ООП.
                            –3
                            присмотритесь к коду:

                                public static function fromValues($hours, $minutes)
                                {
                                    $time = new Time;
                                    $time->hours = $hours;
                                    $time->minutes = $minutes;
                                    return $time;
                                }

                            тут создается объект и ему устанавливаются свойства, как будто они публичные. Хотя они приватные. Это работает потому что интерпретатор смотрит на public/protected/private в контексте класса, а происходит это в том же классе. Если такое поведение интерпретатора поменяется, то и код перестанет работать.
                              +3
                              Если такое поведение интерпретатора поменяется, то и код перестанет работать.

                              С чего бы ему меняться то? Или вы серьезно думаете что это баг?

                              в контексте класса

                              Модификаторы private/protected указывают на то, что с элементами объектов могут работать только объекты того же типа (имя класса) или же в случае protected — любой надтип (наследник класса). То есть мы оперируем тут не понятиями "классы инстансы" а чисто типами объектов.

                              То есть суть модификаторов доступа — "скрыть" знания о деталях реализации объекта какого-то типа. Соответственно объекты одного и того же типа вполне себе спокойно знают об устройстве друг дружки и это нисколички не нарушает инкапсуляцию.
                                –1
                                Я понимаю что это поведение языка. И в описанном выше коде ничего страшного не происходит.
                                Но, допустим класс у нас будет не финальным, а свойства не приватными, а защищенными. И все… приехали. Вы не можете построить систему с немутабельными объектами, потому что любой наследник нашего класса может менять любые защищенные свойства. И вы никогда не можете быть уверенными на 100%, что состояние объекта не изменилось.
                                  +2
                                  А что приехали?

                                  пример кода
                                  <?php
                                  class A
                                  {
                                      private $a;
                                  }
                                  
                                  class B extends A
                                  {
                                      private $a;
                                  
                                      public static function create($value)
                                      {
                                          $instance = new static;
                                          $instance->a = $value;
                                  
                                          return $instance;
                                      }
                                  }
                                  
                                  $object = B::create(42);
                                  var_dump($object);
                                  
                                  /*
                                  object(B)#1 (2) {
                                    ["a":"B":private]=>
                                    int(42)
                                    ["a":"A":private]=>
                                    NULL
                                  }
                                  */


                                  Пых разруливает такие ситуации сам и изолирует приватные переменные внутри каждого отдельного класса, а шторм даже подчёркивает проблемные конструкции, мол "лучше бы переименовать эту переменную"

                                  Просьба разъяснить чуть поподробнее, я немного не понимаю в чём суть проблемы.
                                    –1
                                    И вы никогда не можете быть уверенными на 100%

                                    Именно по этому я в большинстве случаев все делаю private + final.
                                      0
                                      безусловно, если не нужно наследоваться
                                        –1
                                        А наследоваться практически никогда не нужно.
                                          +2
                                          Вот мне серьезно интересует мнение несогласных (минусующих). Наследование — это классная штука, но с ней надо быть аккуратно и не оверюзить его, отдавая предпочтение композиции/декорации.

                                          protected свойства — это вообще кастыль, и нужно 10 раз подумать прежде чем делать что-то protected и уж тем более public. Все, с чем может работать не только объекты конкретного типа, можно воспринимать как публичный и полу-публичный интерфейс, а единыжды став публичными мы уже не можетм так легко его менять. То есть если мы вдруг решили поменять сигнатуру protected метода, и уж тем более public, нам нужно задуматься "какой код мы по итогу сломаем".

                                          Принцип "работает не трогай" — это хороший принцип, protected variations из GRASP и open/close из SOLID как раз об этом. Вместо того что бы что-то менять — добавляй, расширяй. А наследование, protected и т.д в большинстве случаев ведут нас к нарушению всех этих принципов (в неумелых руках, то есть в среднестатистических).
                                            0
                                            Если вы пишите закрытый код, то наверно можно согласиться. Но когда я вижу private в сторонней либе, у меня подгорает. Автор либы в принципе не может знать мои требования, и я не вижу смысла закрывать наследование.
                                            p.s. в java protected действует на namespace, а в PHP на класс, то есть лично я не вижу много смысла в private в PHP. Ну final поставьте, если очень хочется. Но к чему это приведёт? Да скорее всего к copy-paste..
                                            p.p.s. не минусовал
                                              +2
                                              и я не вижу смысла закрывать наследование

                                              У вас будет подгорать еще больше, если автор либы не сможет ничего подправить/улучшить из-за обратной совместимости. Или еще лучше — сломает оную.
                                              Поставьте себя на место разработчика этой опенсурсной либы. Как только мы делаем что-то публичным, или даем возможность наследоваться, мы по сути говорим "оукей, это публичный интерфейс моей библиотеки и я обязуюсь его поддерживать!". И логично что чем меньше этот интерфейс и чем меньше вещей, от которых ждут обратной совместимости, тем проще нам будет жить.
                                              p.s. в java protected действует на namespace

                                              На уровне package, а не namespace все же. Есть разница. В отличии от нэймспейсов во всяких там пыхах или c#, пакеты не имеют вложенности. А еще в рамках пакета у нас может быть один публичный класс и десяток приватных. И при таком раскладе мы вполне себе можем для удобства сделать свойства публичного класса package-private (то есть еще не protected но уже и не private, просто не указывать явно модификатор доступа).
                                              Опять же, если касаться вопроса юнит тестирования того же, тестировать мы будем исключительно публичный класс и его публичные методы. А все что внутри пакета — это деталь реализации. Мы должны экспоузить как можно меньше деталей реализации, дабы иметь возможность безболеззенно в будущем рефакторить код (open/close принцип из SOLID или protected variations из GRASP)
                                              Но к чему это приведёт? Да скорее всего к copy-paste..

                                              Ну если вы делаете DRY исключительно при помощи наследования, то я обычно выношу "дублирование" в общую зависимость. Ибо если у двух моих сервисов есть идентичные методы, стало быть я что-то делаю не так, и видимо нарушил принцип единой ответственности.
                                              Для сущностей или VO я использую наследование, поскольку у этих объектов не может быть не то что "общих" зависимостей, но и в принципе зависимостей. Но опять же это малая часть случаев, когда в принципе у меня возникает дублирование кода и тут проще вынести общие вещи в базовый абстрактный класс, а не убирать final.
                                                0
                                                Да, согалсен. Но как я указал выше, автор 3ей либы не знает моих требований, и предусмотреть все варианты для DRY далеко не каждый может да и не хочет наверно. И потому — лучше пусть будет protected и я сам разберусь что мне с этим делать. Альтернатива мне нравится меньше.
                                                  0
                                                  namespace vs package, разница есть, но мне кажется вы приувеличиваете значение. Хотя может я чего не понимаю.
                                                    0
                                                    А dry я стал последнее время делать через трейты. Хоть в пыхе они и костыльные, надеюсь в будущем поправят. Хотя бы.
                                                  +2
                                                  protected свойства — это вообще кастыль, и нужно 10 раз подумать прежде чем делать что-то protected и уж тем более public.

                                                  На jug.ru, если не путаю, был доклад по поводу того, какого фига джависты всегда фигачат private и добавляют getField\setField, когда можно просто сделать поле публичным. И местами я даже согласился. Плюсы такого подхода очевидны — можно потом запросто добавить какие-то критерии чтения\установки значения, валидацию и прочее, не меняя интерфейса работы с классом, но и минусы тоже есть.

                                                  Я написал это потому, что друзья похапешиники пошли по той же тропе джавы и зачастую переусложняют интерфейсы, ради качества. Я не исключение. Может стоит иногда поплёвывать на важность инкапсуляции и просто стараться писать код наиболее лаконично и читаемо? Я в сомнениях, даже обычный POPO объект, той же самой доктрины может превратиться в 200-строчный класс, состоящий из одних полей + get\set, вместо указания полям паблика. Ну да, плохо. Да, не контролируемо. Но 15 строк кода всяко чище? Да и потом можно навесить private + перехватку через магические методы, если что не так.
                                                    +1
                                                    какого фига джависты всегда фигачат private и добавляют getField\setField, когда можно просто сделать поле публичным.

                                                    И я полностью согласен с этим. При таком раскладе мы как бы… не получаем никакого профита. Мы делаем и неудобно, и инкапсуляцию нарушаем (внешний мир знает все о структуре нашего объекта), и если применять этот подход к бизнес-объектам, мы получим известный антипаттерн — анемичную модель.
                                                    В принципе стоит различать тупые сеттеры, и просто методы, реализующие поведение. Вот тебе пример:
                                                    public function chagePassword(string $password) {
                                                        $this->password = $password;
                                                    }

                                                    И вроде как "какое тут к черту поведение, просот сеттер по другому назвал". И как бы так оно и есть. Но только семантика уже отличается. Этот метод появился у меня не потому что "ну а как же, надо ж сеттер", а потому что у меня есть бизнес правило "User should be able to change password"
                                                    друзья похапешиники пошли по той же тропе джавы и зачастую переусложняют интерфейсы

                                                    Я тут могу сказать только то, что "друзья похапэшники" не понимают что такое ООП, и среди джавистов это так же не редкость. Переусложнение интерфейсов обычно идет от непонимания того, что делает разработчик.
                                                    Может стоит иногда поплёвывать на важность инкапсуляции

                                                    Нет, "сеттеры" — это нарушение инкапсуляции. Как раз таки у простого интерфейса "инкапсуляции" намного больше.
                                                    Я в сомнениях, даже обычный POPO объект, той же самой доктрины может превратиться в 200-строчный класс, состоящий из одних полей + get\set

                                                    А ты смотрел/читал официальную позицию разработчиков доктрины по этому вопросу? никаких сеттеров!
                                                      0
                                                      Принято, мой косяк, недоглядел на счёт сеттеров. Действительно в доках доктрины не нашёл ни одного упоминания setSomething. Мысль на счёт работы с бизнес-логикой, нежели с реализацией — тоже понял.


                                                      Но в любом случае это не отменяет моего тезиса, что иногда лучше сделать поле публичным, нежели реализовывать геттеры.

                                                        0
                                                        иногда лучше сделать поле публичным, нежели реализовывать геттеры.

                                                        Для объектов не содержащих поведения — определенно.
                                                  0
                                                  За что минусы человеку? 99% процентов случаев использования наследования в рядовом коде — это прямое нарушение SRP наращиванием/фиксом имеющегося функционала и/или интерфейса.
                                      +1
                                      мне кажется, что вопрос правильного создания модели не должен лежать в зоне отвественности этой самой модели

                                      Он и не лежит, статический метод-фабрика это лишь упрощение, на которое может пойти разработчик что бы не городить отдельный объект-фабрику, которую надо еще инджектить в качестве сервиса. Да и выразительность выше, и можно разные бизнес правила описывать (я к слову этого в статье не особо наблюдаю). Например вот вам ситуация:

                                      У нас есть два типа покупателей, обычные, и, например, оптовые. У оптовых помимо email + password обязательными так же какие-то дополнительные данные. И мы хотим как-то явно выразить эти бизнес правила в нашем коде.


                                      class Buyer {
                                          // ...
                                          private function __construct(string $email, string $password, string $type) {
                                              $this-&gt;email = $email;
                                              $this-&gt;password = $password;
                                              $this-&gt;type = $type;
                                          }
                                      
                                          public static function register(string $email, string $password) : Buyer {
                                              return new static($email, $password, static::TYPE_REGULAR);
                                          }
                                      
                                          public static function registerAsWholesaler(string $email, string $password, string $phone, string $arr) : Buyer{
                                              $buyer = new static($email, $password, static::TYPE_WHOLESALER);
                                              $buyer-&gt;phone = $phone;
                                              $buyer-&gt;arr = $arr;
                                      
                                              return $buyer;
                                          }
                                      }

                                      таким образом мы получили:



                                      • явное обозначение бизнес правил и ограничений
                                      • нет возможности создать невалидный объект, то есть с инкапсуляцией и protected variations у нас все хорошо.
                                      • UFO just landed and posted this here
                                          0
                                          Чертов хабрапарсер покоцал чуть чуть.

                                          <?php
                                          
                                          class Buyer
                                          {
                                          // ...
                                              private function __construct(string $email, string $password, string $type)
                                              {
                                                  $this->email = $email;
                                                  $this->password = $password;
                                                  $this->type = $type;
                                              }
                                          
                                              public static function register(string $email, string $password) : Buyer
                                              {
                                                  return new static($email, $password, static::TYPE_REGULAR);
                                              }
                                          
                                              public static function registerAsWholesaler(string $email, string $password, string $phone, string $arr) : Buyer
                                              {
                                                  $buyer = new static($email, $password, static::TYPE_WHOLESALER);
                                                  $buyer->phone = $phone;
                                                  $buyer->arr = $arr;
                                          
                                                  return $buyer;
                                              }
                                          }

                                          А что до версии PHP — текущая стабильная версия — 7.0. А если убрать тайп хинтинг то 5.0.
                                          –2
                                          а так же потеряли простой способ замокать класс для теста

                                            +1
                                            А зачем вообще эти классы мокать? Мокать надо сервисы, интерфейсы, то что может иметь множество реализаций. Тут же сущности и объекты-значения, их мокать не нужно.
                                          0
                                          Не дописал в предыдущем комментарии.

                                          мне кажется, что вопрос правильного создания модели не должен лежать в зоне отвественности этой самой модели.

                                          Простите, это как? Это как раз таки прямая зона ответственности модели, она не должна позволять создать себя "неправильно". Ну то есть все обязательные поля — в конструкторе. А поскольку у нас есть принцип protected variations а конструктор в PHP у нас только один — единственный разумный вариант инкапсулировать ограничения внутрь объекта — статические методы фабрики.

                                          А вопрос создания объекта делегировать на более высокий уровень.

                                          И мы переходим к такому понятию как Creator. То есть на каждую сущность в нашей системе нам надо буде завести по объекту-фабрике. У которой даже зависимостей нету. Ну ок, вместо объектов можно завести просто функции, хорошо. Стоп, а если можно просто функции, то почему бы не сделать эти функции привязанными к нужному контексту что бы небыло путаницы?.. Возвращаемся к статическим методам фабрикам.

                                          Допустим у Вас в системе могут появлятся пользователи из формы регистрации на вашем сайте, а могут быть импортированы из системы партнера.

                                          И это будет два статических метода фабрики.

                                          Вы можете создать класс-менеджер, который будет уметь привести данные с формы/с csv/с запроса на апи в общий вид и создать объект пользователя.

                                          А можно этого не делать. Просто так лепить классы-менеджеры это как бы не очень хорошо. Все же у нас для всего должен быть смысл. А смысла просто так лепить объект, который внутри просто сделает новый инстанс другого объекта без какого либо дополнительного поведения — ну это как бы ниочем.
                                            0
                                            А можно этого не делать. Просто так лепить классы-менеджеры это как бы не очень хорошо. Все же у нас для всего должен быть смысл. А смысла просто так лепить объект, который внутри просто сделает новый инстанс другого объекта без какого либо дополнительного поведения — ну это как бы ниочем.

                                            смысл дополнительного класса создателя в том, что его легко и просто замокать, чего не скажешь о статических методах самой модели. да и о любых статических методах вцелом
                                              0
                                              Повторюсь — мокают то, что не относится к тестируемому поведению. Мокать объект юзера это как мокать DateTime тот же.

                                              Вообще при написании тестов мокать нужно как можно меньше дабы наши тесты как можно меньше знали о внутреннем реализации объектов системы. Потому обычно мокают только зависимости наших сервисов. У объекта User зависимостей нет, он представляет состояние системы. Так же User не может быть зависимостью тестируемого сервиса.

                                              Пройдите по ссылке, там в оригинале Матиасу задают вопросы примерно того же уровня и он там популярно все объясняет.
                                                0
                                                Допустим мы тестируем некий сценарий в процессе которого в зависимости от входящих данных создается та или иная сущность. те же обычный покупатель / оптовый покупатель. На нужно протестировать что сценарий конкретно реагирует на входящие данные и создает нужную сущность. Но, внезапно оказывается, что у сущности не 2-3 поля, а несколько десятков, а то и сотен. И что вариантов создаваемых объектов тоже не 2, а гораздо больше. И создавать их по сути то в тесте и не надо. Надо только проверить, что сценарий конкретно выбирает класс, который нужно создать. Вот тут вполне пригодятся легковесные моки этих же классов.
                                                  +1
                                                  Вот тут вполне пригодятся легковесные моки этих же классов.

                                                  Нет не пригодятся.

                                                  Но, внезапно оказывается, что у сущности не 2-3 поля, а несколько десятков, а то и сотен.

                                                  Какая разница? с точки зрения теста разницы нет никакой. И мокать опять же смысла нет, так как нам проще проверить тип возвращенного инстанса или были ли исключения и т.д.
                                                  В качестве мысленного эксперемента. Приведите пример ситуации, которую вы описываете. Например сущность с двумя десятками полей. А я в ответ приведу свой пример без моков и вы увидите что разницы никакой нет, и мой вариант теста будет проще.
                                                    –1
                                                    сущность отчет, которая содержит сотни собранных метрик. разница в объеме потребляемой памяти и скорости выполнения тестов. на большом проекте после каждого мержа бранчи с фичей прогоняются тысячи тестов. в релиз идут сотни бранчей. релизы дважды в день. да, подождать одну секунду или 10 — небольшая разница. но умножьте на количество прогонов и посчитайте...
                                                      0
                                                      сущность отчет, которая содержит сотни собранных метрик.

                                                      Вы хотите сказать что простой ассайн 100-ни пропертей выходит дороже рефлексий? И повторюсь — приведите пример такого теста, где вы сущности мокаете. Мне просто интересно посмотреть как вы вообще это делаете. Вы же выходит конструктор мокаете.

                                                      подождать одну секунду или 10 — небольшая разница

                                                      Я подозреваю что разницы вообще нет, и даже так, что моки в приведенном вами случае будут работать даже медленнее. Интроспекция это не дешевое развлечение.
                                                        0
                                                        прошу прощения. я наверно выразился не совсем ясно.
                                                        замокаем мы класс менеджера. у менеджера всего несколько методов, которые возвращают нужный тип сущности (обычный юзер, оптовый юзер, etc). мы замокаем их так, чтобы они возвращали не настоящую сущность, а её стаб. стаб будет легковесным, потому что забьет на все данные, ничего в себя сетить не будет, и ничего валидировать также. разница в них только в типе, который мы и будем проверять в тесте.
                                                        есть сценарий который в зависимости от ряда параметров должен вернуть одну из реализаций юзера. мы знаем на каких входных парамерах какой тип ожидается. и можем проверить, что сценарий возвращает нам нужный тип, при этом не создавая тяжелых моделей.

                                                        или второй вариант. когда не создается новая сущность, а берется старая из бд. Вы опять же, можете замокать методы менеджера так, чтобы он ни в какую бд не лез, никакие сущности оттуда не тянул, а возвращал вам стаб нужного типа.
                                                          0
                                                          у менеджера всего несколько методов, которые возвращают нужный тип сущност

                                                          Вообще сущности за пределы менеджера выходить не должны (например в контроллер). То есть если мы мокаем "менеджер" — мы ничего не знаем о сущностях так как это деталь реализации менеджера.

                                                          Для выборок вида:

                                                          $reportManager->getReport(PremiumPurchasesSpecification::new());

                                                          да, конечно же имеет смысл наш $reportManager замокать. Но вот если у нас есть метод

                                                          $userManager->register($userRegistrationDTO);

                                                          то тут как бы мокать нечего. И уж темболее мы не будем мокать сущности. Скорее всего мы бы замокали репозиторий, который является зависимостью нашего менеджера и будем ожидать что будет вызван метод add() с аргументом определенного типа.
                                                    0
                                                    Чем класс Buyer отличается от типов данных Array или Integer? Почему Buyer нужно мокать — а Array или Integer не нужно?
                                          • UFO just landed and posted this here
                                              +8
                                              Работа со временем — просто частный случай объекта реализующего бизнес-логику.

                                              Вы так говорите, как-будто фабричные методы это что-то плохое.
                                              • UFO just landed and posted this here
                                                  +3
                                                  (уточню, это перевод)

                                                  Показан лишь подход к работе, а не конкретный сниппет.
                                                  • UFO just landed and posted this here
                                                      +10
                                                      image
                                                      За столь неочевидный гуй пинайте Денискина.
                                                      • UFO just landed and posted this here
                                                        • UFO just landed and posted this here
                                                            +3
                                                            image
                                                            Что не так-то?
                                                              +6
                                                              Ой да никто не читает статьи просто. Успокойтесь. Заголовок прочитали, код мельком глянули — значит можно в комментарии бежать.
                                                              • UFO just landed and posted this here
                                                                  +3
                                                                  да не потерялся смысл ни капельки. Просто как я уже говорил, большинство статьи наискосок читают.
                                                                  • UFO just landed and posted this here
                                                        0
                                                        Присмотритесь, он указан в статье. Продублирую для Вас verraes.net/2014/06/named-constructors-in-php.
                                                  +4
                                                  Во-первых в PHP есть DateTime

                                                  Ок, давайте рассмотрим на примере DateRange:

                                                  DateRange::fromString('within 5 days');
                                                  DateRange::week();
                                                  DateRange::between(new DateTime(), new DateTime('+5 days'));
                                                  +2
                                                  Вставлю 5 копеек.

                                                  Конструктор получает одного формата время, преобразование времени в этот формат происходит на месте, перед созданием объекта.

                                                  Или… нет, запихивать в класс Time конвертер из 100500 форматов в один как-то не очень. Лучше пусть конвертер будет отдельно от класса Time, и его можно будет отдельно редактировать, добавлять новые форматы.

                                                  Получается так: есть время в каком-то виде, конвертер переводит его в нужный формат, форматированное время скармливается конструктору.
                                                    0
                                                    Лучше пусть конвертер будет отдельно от класса Time

                                                    Можете уточнить чем именно это лучше?

                                                    Мне реально интересно. Просто вот конкретно сейчас работаю над небольшим проектом на Laravel в котором есть много работы с датой и временем и есть там отличный класс Carbon, который, на сколько я понимаю, как раз и использует подобные фабричные методы. У меня на входе часто бывает дата и/или время в строковом формате и очень удобно делать вызов в таком виде

                                                    $date = Carbon::parse($someStringWithDateTime);

                                                    Все читабельно, лаконично и понятно. И я вот не вижу вообще никакого преимущества в использовании класса конвертера, я уже молчу о том, чтобы каждый раз распарсивать строку самому (повторюсь, что входные параметры бывают в разных форматах, я только знаю, что они 100% валидные).
                                                      0
                                                      Ну да. В данном случае форматированием занимается Carbon::parse.

                                                      Здесь класс Time — для примера, чтобы показать, как использовать именованные конструкторы, а не для того, чтобы его использовать вместо стандартных средств.
                                                      Это было для случая, когда перед созданием объекта класса Time есть данные разных форматов: ("11:45"), (11, 45). И чтобы не перегружать класс Time конвертерами, их вынести в другое место (функцию или класс).
                                                        0
                                                        Пользоваться удобно. А вот заниматься поддержкой кода самой библиотеки Carbon я бы не рискнул, там все получилось несколько запутанно :-)
                                                          0
                                                          Пробежался по коду, пробежался по тестам — где там запутанность?
                                                            0
                                                            Мм, да, щас вполне сойдёт. Либо раньше было хуже, либо я с чем-то другим аналогичным перепутал.

                                                            Но переусложненный конструктор и статики для «testing aids» мне все равно не нравятся. Да и в целом это все сахарок.
                                                            В рамках domain model я вообще предпочитаю ограничиваться DateTimeImmutable.
                                                              0
                                                              Но переусложненный конструктор и статики для «testing aids» мне все равно не нравятся.

                                                              Статические методы-фабрики это чистые функции. Они не несут в себе никаких сайд эффектов. Их тестировать — легче легкого.
                                                              предпочитаю ограничиваться DateTimeImmutable.

                                                              К сожалению DateTimeImmutable не дает той выразительности и гибкости. Хотя Carbon с другой стороны мутабельный, потому я использую chronos. Да и не забываем о таких чудесных вещах как таймзоны, отдельные даты без привязки ко времени (тут удобно иногда обертку сделать) и DateRange-и всякие. Не стоит вообще ограничивать себя, это может навредить больше чем спасти. Главное здравомыслие.
                                                        0
                                                        конвертер будет отдельно от класса Time, и его можно будет отдельно редактировать, добавлять новые форматы.

                                                        Кстати одно другому не мешает: может быть тот же фабричный метод "parse", внутри которого используется такой конвертер. И можно будет так же "отдельно редактировать, добавлять новые форматы", при этом не теряя на удобстве использования.
                                                          0
                                                          Если понять статичные методы "заменяющие" именованные конструкторы я могу, то вот вызывать что либо в себе эти методы никак не должны, тем более статические, ибо тут уже будет бешеная связанность кода, которую поддерживать будет вообще не возможно.
                                                        +1
                                                        explode($timeOrHours, ':', 2);

                                                        1) разделитель — это первый параметр
                                                        2) лучше это написать через sscanf($timeOrHours, '%2d:%2d')
                                                          0
                                                          А может стоить использовать множественный конструктор, с передачей класса в основной конструктор (примерно как в C++)?
                                                          Приведу пример. Тут вот человек немного касается сути множественных конструкторов. Развивая идею, мы можем проверить класс объекта переданного в конструктор. При передаче в конструктор объекта можем его закастовать. Т.е. оставляем базис из вышеуказанного комментария, дополнительно определяя классы для разных типов передач, пишем для каждой конструктор, а в конструкторе нашего класса определяем переданный класс и в зависимости от него вызываем нужный конструктор класса.
                                                          В итоге избавляемся:



                                                          • от статических методов
                                                          • от необходимости помнить название функций для создания
                                                          • скртия конструкттора
                                                            0
                                                            Получаем:

                                                            единый конструктор, который всегда вызывается
                                                            четкую типизацию
                                                            проще понимание кода

                                                          Only users with full accounts can post comments. Log in, please.