Pull to refresh

Comments 97

Как заметил oxidmod, если у нас есть более высокий уровень, которому можно делегировать вопрос создание объектов, то текущий подход не очень-то и применим.

Тут больше раскрывается вопрос инкапсуляции логики создания объекта.
Автор ( и я его поддерживаю) предлагает использовать именованные конструкторы ( в частной реализации PHP это статические фабричные методы) для объектов доменной области, а в них DI не нужен.
А если уж очень нужно иметь сервис с разными конструкторами ( сложно представить такую ситуацию), то в том же Symfony DI есть поддержка фабрик.
Меня интересовало, что будет инжектиться в класс, завязанный на класс 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. Вообщем, мне эти "именованные конструкторы" как-то не совсем по душе. Но смотрятся красиво, не отрицаю.
Прошел по ссылке на оригинальную статью. Насколько я понял в "вопросах-ответах" Матиас считает, что этот подход хорош в самых простых объектах, которые и тестировать-то нет неообходимости. Ну что ж, каждое решение имеет свою область применения.
которые и тестировать-то нет неообходимости

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вполне возможно, в таком случае у меня бы не возникло вопроса, как инжектить объект класса Time (или integer, как вы резонно заметили).
За других не скажу, а у симфонии с этим проблем на моей памяти не возникало.
Проверил.
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()
    {
    }
Мельком взглянул исходники, должно вот так завестись:
$container = new ContainerBuilder();
$container->setDefinition('time', (new Definition())->setFactory('Time::create'));

$obj = $container->get('time');

Просьба симфонистов поправить, если что не так.
Да, спасибо, так работает.
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-фреймворках объектов с приватным конструктором.
фабрике можно аргументы задавать. То есть если сильно извращаться — можно крутить как хочешь. Вот только я бы по рукам бил за желание пихать такие объекты в конструктор сервисов.
UFO just landed and posted this here
Да и обращение к приватным полям объекта извне этого объекта является больше хаком и не факт что так будет работать всегда.

А где в предложенном подходе обращение к приватным полям объекта извне? Все происходит внутри самого объекта и полностью согласуется с ООП.
UFO just landed and posted this here
Если такое поведение интерпретатора поменяется, то и код перестанет работать.

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

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

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

То есть суть модификаторов доступа — "скрыть" знания о деталях реализации объекта какого-то типа. Соответственно объекты одного и того же типа вполне себе спокойно знают об устройстве друг дружки и это нисколички не нарушает инкапсуляцию.
UFO just landed and posted this here
А что приехали?

пример кода
<?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
}
*/


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

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

Именно по этому я в большинстве случаев все делаю private + final.
UFO just landed and posted this here
А наследоваться практически никогда не нужно.
Вот мне серьезно интересует мнение несогласных (минусующих). Наследование — это классная штука, но с ней надо быть аккуратно и не оверюзить его, отдавая предпочтение композиции/декорации.

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

У нас есть два типа покупателей, обычные, и, например, оптовые. У оптовых помимо 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
Чертов хабрапарсер покоцал чуть чуть.

<?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.
UFO just landed and posted this here
А зачем вообще эти классы мокать? Мокать надо сервисы, интерфейсы, то что может иметь множество реализаций. Тут же сущности и объекты-значения, их мокать не нужно.
Не дописал в предыдущем комментарии.

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

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

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

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

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

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

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

А можно этого не делать. Просто так лепить классы-менеджеры это как бы не очень хорошо. Все же у нас для всего должен быть смысл. А смысла просто так лепить объект, который внутри просто сделает новый инстанс другого объекта без какого либо дополнительного поведения — ну это как бы ниочем.
UFO just landed and posted this here
Повторюсь — мокают то, что не относится к тестируемому поведению. Мокать объект юзера это как мокать DateTime тот же.

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

Пройдите по ссылке, там в оригинале Матиасу задают вопросы примерно того же уровня и он там популярно все объясняет.
UFO just landed and posted this here
Вот тут вполне пригодятся легковесные моки этих же классов.

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

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

Какая разница? с точки зрения теста разницы нет никакой. И мокать опять же смысла нет, так как нам проще проверить тип возвращенного инстанса или были ли исключения и т.д.
В качестве мысленного эксперемента. Приведите пример ситуации, которую вы описываете. Например сущность с двумя десятками полей. А я в ответ приведу свой пример без моков и вы увидите что разницы никакой нет, и мой вариант теста будет проще.
UFO just landed and posted this here
сущность отчет, которая содержит сотни собранных метрик.

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

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

Я подозреваю что разницы вообще нет, и даже так, что моки в приведенном вами случае будут работать даже медленнее. Интроспекция это не дешевое развлечение.
UFO just landed and posted this here
у менеджера всего несколько методов, которые возвращают нужный тип сущност

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

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

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

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

$userManager->register($userRegistrationDTO);

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

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

Показан лишь подход к работе, а не конкретный сниппет.
UFO just landed and posted this here
image
За столь неочевидный гуй пинайте Денискина.
UFO just landed and posted this here
UFO just landed and posted this here
Ой да никто не читает статьи просто. Успокойтесь. Заголовок прочитали, код мельком глянули — значит можно в комментарии бежать.
UFO just landed and posted this here
да не потерялся смысл ни капельки. Просто как я уже говорил, большинство статьи наискосок читают.
UFO just landed and posted this here
Во-первых в PHP есть DateTime

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

Sign up to leave a comment.

Articles