Pull to refresh

Comments 238

Вопрос с гетткрами и сеттерами восходит к Java Bean где они используются повсеместно.И если в php по крайней мере они помогают с типизацией в Яве поля и без этого типизированы и их применение ближе к традиции чем к рационально у выбору.


Что касается доктриновских сущностей. То как мне кажется добавлять в них логику более тяжёлую чем геттеры и сеттеры плюс Некоторые традиционные декораторы типа translatable и т.п. решение не очень перспективное. Просто зачастую разработчики не реализуют класс тяжёлой модели в которой как раз и задана логика того же delivery свойства и вызывает геттеры и сеттеры напрямую в сервисах.

Спасибо большое. Да, наслышан про бины, про историю EJB и POJO :)

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

Кроме того есть ряд доменов — простых и лаконичных с точки зрения описания в модели, но привычка — дело страшное и все летит к чертям :( Об это и написал.
То как мне кажется добавлять в них логику более тяжёлую чем геттеры и сеттеры плюс Некоторые традиционные декораторы типа translatable и т.п. решение не очень перспективное.

Такое утверждение таки требует аргументации )

В статье приведены хорошие аругменты в пользу rich domain model, и отказа от анемии, включая то что анемия — возврат к старому доброму процедурному коду и утеря контроля над состоянием.

Разобраться в типичном проектике с 3-мя папками со свалкой нескольких десятков сущностей, сотней сервисов и представлений, когда сущности представляют собой простыню геттеров-сеттеров занятие уже не особо преспективное. Это сложности для ввода новичков в проект и сильная привязка проекта к команде, которая может(по крайней мере пока) держать в уме реализованную в проекте БЛ. А если на проекте не останется людей которые его начинали ситуация ещё усложнится.

По поводу translatable и т.п. — в итоге получаем то, что сущность представляет почему-то не доменную модель, а отражает UI-приложения(Поля сущности = интерфейс приложения, всякие translatable — исключительно UI). Я уж не хочу думать что будет когда в приложении появится несколько интерфейсов.
Конечно при таком подходе совмещать в тех же сущностях БЛ малоперспективное занятие. И решается это проведением границ между отображениями и логикой — для UI/GUI есть DTO/структуры/массивы. Для выборок есть SQL, для выборок по полям сущностей доктрины есть DQL, и результаты DQL запроса вовсе не обязательно мапить на сущности.

Ещё одна важная проблема анемичных моделей — юнит тесты. Анемичная модель это +1 зависимость во все тесты. Их будет сложнее читать/писать/поддерживать, поэтому про них бывает просто забывают и поступают как со старым легаси: добавил сервис — написал пачку интеграционных тестов и сойдёт. Недостатки интеграционных тестов перед юнитами я думаю не тема данного комментария и понятны тем кто их пишет.

Пользовательский интерфейс — это не та вещь, по которой нужно проводить границы в приложении(сразу вспоминается Тостер с вопросами аля «Разделил админку и фронт на два приложения, подскажите как реюзать сущности?»).

Переводы могут быть бизнес-требованиями. Типа "наименование контрагента должно быть известно на трёх языках"

Обеими руками за то, чтобы сеттеров не было в сущностях в принципе, по крайней мере технических не было просто как средства установить значение внутреннего поля, а логика изменений состояния была инкапсулирована в доменных методах.


Но зачастую приходится их делать, для того, чтобы не вводить ещё один слой абстракции из-за того, что тулинг типа обработчиков форм не умеет работать с внутренними свойствами. Кстати, не понятно зачем в статье Symfony упомянут — ничего специфичного нет.


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


P.S. Для геттеров не помешал бы "правильный" пример.


P.P.S. Наверное, имелась в виду 'order === null'

Конкретно для форм есть такая штука github.com/sensiolabs-de/rich-model-forms-bundle.

По теме статьи, полностью согласен. Как не смешно, из за этого ушел из PHP разработки. Около двух лет назад пытался довести аналогичные доводы до команды, объяснить что используя сеттеры сами себе ставим палки в колеса, что такое инвариант, как его сохранить и тд. Дело оказалось крайне не благодарным, когда я заводил эту тему (не один раз) на меня смотрели с неслабой такой долей скепсиса. Плюнул на это дело, нашел другую работу, в требованиях написано DDD, на второй день задал вопрос тимлиду: «почему в коде только анемичные модели, DDD то будет?» на что получил ответ: «что такое анемия?» Через пару тройку месяцев ушел в go…

И как на go с DDD? Я на одном сервисе попробовал как-то не очень результат нравится.

С переходом на go задачи стали намного ближе к системному уровню чем к бизнес-прикладному. Тем не менее если надо что-то такое соорудить берем go-kit. Он фактически навязывает архитектуру портов-адаптеров и косвенно навязывает lite-ddd подход.
Если смотреть по выразительности языков, то тут по моему мнению php выигрывает, соответственно и для ddd он лучше подходит (для полного счастья не хватает модификаторов области видимости на уровне модулей)

Спасибо за наводку, гляну. Мне больше близки именно задачи бизнес-прикладного характера.

Меня как раз удерживает переход на go работа с базами данных. По сравнение с доктриной там сразу и все очень сложно. Особенно когда идёт речь о nullable которые из-за строго йтипизации приходится реализовывать структурой

Я кроме gorm ничего не использовал, но в его контексте как-то непонятно, что реализовать структурой, тип поля или всю запись? Если поля, то не структурой, а указателей рекомендованных способ. А структура для записи в целом

Т.к. go типизирванный язык то значение из базы данных null вызывает ошибку что ожитбается значение строки а фактическое знеачение nil Поэтому все такие поля нужно определять как например sql.NullString и т.п. И работа с этими полями становится сразу и очень напряжной.

Наверно, лучше сказать «ушел из бизнес-разработки в более системную». Так-то есть проблемы с использованием DDD в зависимости от команды, а не ЯП (хотя практики и инфраструктура тоже влияет, конечно).

думаю без знака восклицания будет правильно


if ($this->isDelivered()) {
    throw new OrderDomainException('Order already delivered.');
}
два чаю этому господину :) спасибо!
Инкапсуляция это не сокрытие данных, это обьединение данных с методами над ними
Тут она нарушается потому что логика работы над обьектом уходит наружу
Да :) разницу между сокрытием данных и инкапсуляцией знаю, но все же в нее объединяют кроме логики еще и сокрытие данных (часто), потому этим моментом тут пренебрег!
Вы правы!

Maksclub Во-первых спасибо за статью, действительно это не самый однозначный (и широко освещаемый) вопрос.


Насколько я могу понять — описываемый вами подход ранее описывал Marco Pivetta в своём докладе "Doctrine ORM Good Practices and Tricks" (записи докладов есть у него на сайте, вот эта к примеру).


Во время просмотра его доклада и во время чтения вашей статьи, хотя я концептуально согласен и с аргументами Marco и с вашими — у меня тем не менее возникает несколько вопросов (возможно из-за неполного понимания), буду благодарен если вы их прокомментируете:


Первый вопрос:


В вашем примере "неубиваемого" объекта Order как будет выглядеть код, отображающий этот Order во view? Свойства объекта у вас приватные, методы реализуют операции над данными, но как отобразить эти данные пользователю?


Правильно ли я понимаю что (возможно) предполагается реализация некоего метода который будет возвращать данные entity в виде, необходимом для отображения? Или же несколько подобных методов на случай если требуется разная детализация данных.


К примеру в случае какого-нибудь Article мы можем хотеть для одного сценария иметь возврат только базовых данных, а в другом — более полных которые в свою очередь могут включать в себя данные из связанных entities (те же комментарии или имя автора), дёргать которые (и тем самым инициировать их загрузку из базы) в более простом сценарии нам не надо чтобы минимизировать количество запросов к базе данных.


Не придём ли мы в этом случае к ситуации что наша entity будет слишком много знать о том каковы сценарии её использования в приложении?


Второй вопрос:


Вы приводите пример метода для изменения статуса ордера, Marco приводит пример с пользователями и ролями. Хотя в ваших примерах в целом всё понятно — я не могу отделаться от ощущения что в общем случае при каких-то сценариях реализация этих методов для работы с данными entity может потребовать использования какого-либо внешнего объекта. В этом случае перед нами встаёт вопрос dependency injection для entity. Он, конечно, решаем, но на уровне ощущений не выглядит чем-то правильным.


Было бы очень хорошо чтобы вы раскрыли этот момент более подробно.

Давайте сразу предупрежу — я не очень опытен, особенно касательно RichModel/DDD и всех этих дел. Потому тему свою ограничил инкапсуляцией, тк просто видел много кода, который становился сложным просто так, бесплатно и просто вижу те момент, где действительно можно упростить обслуживание кода в перспективе и контролировать его сложность.

По поводу сериализации сделал оговорки в статье для отступления на случай таких вопросов:
Конечно, тут не говорю про методы, которые нам нужны для транспорта данных (для чтения и создания DTO, для сериализации и т.д.).

То есть ответом на вопрос про Order — я бы использовал все же геттеры, просто жестко контроллировал их применение именно транспортом.
По этому кстати поводу знаю горячий дискус в Java тусовке с появлением Егора Бугаенко (который про объект могучий и прочее), он за то, что объект сам себя знает как сериализовать, тут я сторонник классических подходов.

Данный доклад Марко посмотрю, не смотрел. Спасибо большое!

Большое спасибо, буду разбираться дальше :)

По второму вопросу — не смогу на данный момент ответить.
Fesor мог бы подсказать по этому поводу
Насколько я понимаю, во многом из-за него мы и имеем «глупые» сущности, которые, по сути, являются лишь хранилищами состояния, делегируя его управление во внешние сервисы. Это вызвано стремлением избежать превращение сущностей в «божественные объекты». В конце концов от active record с переходом на doctrine2 отказались по вполне определенным причинам.

Скорее это вызвано ошибочным (в рамках ООП) разделением ответственности хранилища состояния и его контроля и/или неправильным пониманием, что такое сущность. Типа если состояние и логика сущности будут в одном объекте, то у этого объекта будет две ответственности. Хотя назначение объектов объеденять состояние и поведение.

Я думаю у такого подхода есть свои минусы. Например в случае с глупой сущностью процесс публикации статьи выглядит так:
$service->publish($article);

В предлагаемом вами варианте:
$article->publish();

При этом в процессе публикации статьи может осуществляться проверка прав пользователя, запись логов, отправка письма админу, смена статуса статьи (который тоже может быть объектом), сохранение обновлений в БД и н прочих действий. И не дело сущности знать и думать обо всем этом. Да и инициировать все эти процессы, по идее — тоже.

Естественно не дело. Поэтому метод паблиш статьи знает только то, что касается самой статьи. Например, проверит что статус допускает публикацию, выставит дату публикации, удалит префикс wip из заголовка и т. п. А остальное сделает сервис, вызвав в какой-то момент метод статьи., а не дергая 100500 геттеров и сеттеров. Которые юнит-теста и нормально покрыть может и не получится, кстати.

А я думаю, что развязывание publish() и части описанного (логи, уведомления и пр., кроме сохранения в БД и безопасности) надо решать в том числе с помощью паттерна «Наблюдатель» и шины событий: внутри publish() надо генерить в шину событие. Проверка прав — это пусть контроллеры делают или middleware.
ваше утверждение не конфликтует с тем, что написал VolCh

Одно другому не мешает. На практие именно на PHP часто такая (очень грубо) схема:


class PublishService 
{
  public __constructor(PostRepository $repository, EventBus $bus) {}

  public publishPost(PostId $postId) {
    $post = $this->repo->get($postId);
    $post->publish();
    $events = $post->takeEvents();
    $this->bus->send($events);
}
}
Ну да, только я события в шину внутри сущности пихаю, мне так удобнее). Шину через SL получаю в конструкторе сущности.

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

Кто же этот подход «классическим» объявил? Это сложилось из-за того что в Doctrine ORM нет возможности в сущности ничего внеднять через конструктор.

Так туда и не надо ничего внедрять, кроме того, из чего состоит непосредственно сущность.

Отсюда и проблема анемии сущностей. В том виде, в котором сейчас принято работать с Doctrine ORM: «не надо туда ничего внедрять, кроме того, из чего состоит сущность», проблема анемии останется там где и была.
Тут уже писали — было поведение в сущности, изменились требования, теперь это поведение требует зависимость, поведение переезжает в сервис.
Отсюда и проблема анемии сущностей. В том виде, в котором сейчас принято работать с Doctrine ORM: «не надо туда ничего внедрять, кроме того, из чего состоит сущность», проблема анемии останется там где и была.

У вас попутаны причина и следствие.
Внедрять сервисы в сущность в принципе нехорошо. Что с доктриной что без неё.

Тут уже писали — было поведение в сущности, изменились требования, теперь это поведение требует зависимость

Бизнес-требование само по себе никогда не «требует» зависимость. Зависимость может требовать выбранная вами реализация, которую вы по непонятным причинам посчитали единственно верной, и выстраиваете из этого свою позицию, будто бы подразумевая что альтернитив не существует, что на мой взгляд не корректно.

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

P.s. Почитал ветку ниже. Ещё раз — суть сих действий в том чтобы изолировать изменение состояния и важные бизнес-правила внутри наших сущностей, рич модел не о том чтобы весь код приложения находился в сущностях.

А где тогда должна быть бизнес логика, которая юзает зависимости, например, если при рассчете времени доставки заказа нужно опросить внешнее API? В приложении этот API предположим, представлен как DeliveryService внутри которого http клиент. Можно ли его внедрять в доменные сервисы?

Как я выше писал — бизнес логика не юзает зависимости.
Сходить во внешний сервис — это не бизнес-логика, это простейшая координация действий. Не происходит изменения стейта, нет риска нарушения инвариантов, это простое чтение данных. Этим может заниматься сервис, для этого нам не нужна наша сущность, и поэтому анемичной она не станет. Если результат из сервиса нужен для логики сущности — вызовите метод сущности передав туда результат который вернул сервис.
Сходить во внешний сервис — это не бизнес-логика, это простейшая координация действий. Не происходит изменения стейта, нет риска нарушения инвариантов, это простое чтение данных. Этим может заниматься сервис, для этого нам не нужна наша сущность, и поэтому анемичной она не станет. Если результат из сервиса нужен для логики сущности — вызовите метод сущности передав туда результат который вернул сервис.


А если мне на основании стейта нужно принять решение, ходить ли наружу или нет, а по результатам хождения наружу нужно тоже изменить стейт, здесь тоже предложите в сервис вынести?
Пример требований:
При проверке статуса счета (Invoice), если счет еще не был закрыт, нужно проверить его состояние в платежной системе, проверить соответствие суммы и, если все верно, изменить параметры и состояние счета, а потом закрыть его. Я это делаю в методе Invoice::checkStatus(), в Invoice внедрен шлюз к ПС, который и используется для этой проверки. Это по вашему не бизнес-логика, а «координация действий»? Удивительный темин, кстати.

Внедрять сервисы в сущность в принципе нехорошо. Что с доктриной что без неё.


Мне еще не разу не смогли привести конкретный пример, когда это реально вредило бы.

Внедрять сервисы в сущность в принципе нехорошо. Что с доктриной что без неё.


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

рич модел не о том чтобы весь код приложения находился в сущностях.


Я нигде не утверждал подобное. Снова ваши додумки.
При проверке статуса счета (Invoice), если счет еще не был закрыт, нужно проверить его состояние в платежной системе, проверить соответствие суммы и, если все верно, изменить параметры и состояние счета

Это уже не просто решение на основе стейта, а полноценный процесс затрагивающий несколько операций, и в котором нужно контроллировать консистентность состояния в двух системах.

Почему не делать это в сущности — сущность представляет некую часть вашего и делает какие-то полезные вещи, синхронизация конекстов не её задача. Вот и всё.

Для описания сложных процессов есть Саги/Процесс-менеджеры, по сути это тоже сервисы приложения, просто формально чуть другое предназначение, может быть состояние(ивенты), но это сложно/надо прикручивать/большинству незнакомо(больше рисков сделать лишь хуже) и в принципе для описанного кейса оверинжиниринг, можно сделать просто сервис куда поместить логику синхронизации счетов.

Это всё не противоречит rich-domain-model и может с ними сосуществовать. Просто не нужно переходить из крайности в крайность, и как только появилась логика в сервисах впиливать геттеры/сеттеры в сущности.
Это уже не просто решение на основе стейта, а полноценный процесс затрагивающий несколько операций, и в котором нужно контроллировать консистентность состояния в двух системах.


Что то вы опять нафантазировали. Какую консистентность контролировать? Просто спросить, оплачен ли счет, и на основе этого изменить стейт.

class Invoice 
{
    private $payment_gateway;

    public function checkStatus()
    {
         if (!$this->isClosed()) {
             $data = $this->payment_gateway->getData($this->id);
             
             if ($this->validate($data)) {
                 $this->payment_method = $data['payment_methiod'];
                 $this->close();
             } 
             
         } 
    }
     
}


И полезная вещь здесь — проверка статуса платежа.
А вы AppService, оверинжиниринг это будет. Вместо одного простого метода целый класс прикладного сервиса, с регистрацией в контейнере естественно))).

А зачем счёту знать что-либо о платёжном шлюзе? В вашем случае именно так и получается. По-моему, тут зависимости ровно в другую сторону должны быть направлены. А еще лучше, если счёт не будет ничего знать о шлюзе, а шлюз о счёте.

А вы попробуйте реализовать, то что вы сказали «счет ничего не будет знать о шлюзе, шлюз о о счете», посмотрим насколько громоздким и объектно-оринтированным будет этот код.

Волне нормальным. Сервис, у которого в зависимостях шлюз и, опционально, репозиторий инвойсов. И в методе checkStatus параметром инвойс или его ид (тогда репозиторий и нужен в заисимостях)


Сейчас у вашего инвойса минимум две отвественности: своё состояние и его синхронизация с внешним сервисом, по какому-то внешнему запросу. То есть кто-то вне инвойса знает, что этот инвойс не самодостаточен, может быть рассинхронизирован с чем-то, но типа не знает с чем, хотя и принимает решение в какой момент времени нужно синхронизироваться. Почему вообще не вынести логику синхронизации вовне полностью?

Сейчас у вашего инвойса минимум две отвественности: своё состояние и его синхронизация с внешним сервисом, по какому-то внешнему запросу.

Свое состояние это не ответственность. Ответственность это поведение.

Волне нормальным. Сервис, у которого в зависимостях шлюз и, опционально, репозиторий инвойсов. И в методе checkStatus параметром инвойс или его ид (тогда репозиторий и нужен в заисимостях)


И везде где нужно проверить статус, придется обращаться к этому сервису, передавая ему уже имеющийся инвойс или id

CheckService::checkInvoice($invoice);

Это процедурная декомпозиция.

Вот ваш метод close() реализует поведение сущности. Инвойс знает как себя закрыть. когда закрывать — вне области его отвественности.


Объектная декомпозиция:


class InvoiceSyncService {
  public __constructor(PaymentGate $paymentGate) {...}

  public syncWithPaymentGate(Invoice $invoice): void {
    if ($invoice->isClosed()) return;
    $data = $this->paymentGateway->getData($invoice->id);
    if (!$this->validate($data)) return;
    $invoice->closeOnPayment($data['paymentMethod']);
  } 
}
И все таки мне кажется что если так с Invoice обходиться, то получится, что он будет почти что структурой данных, ведь методы isClosed() и close() это практически геттер и сеттер, не будет ли этого поведения слишком мало (анемия)?:

class Invoice 
{

    private const CLOSED = 100;

    pubic function isClosed() : bool 
    {
         return $this->status == self::CLOSED;
    }

    public function close() {
         $this->status = self::CLOSED;
         $this->triggerEvent(new InvoiceClosedEvent($this->id, $this->amount));
    }

}

В каких-то случаях да, мало поведения будет. И практически те же геттеры/сеттеры с технической точки зрения.


Но с точки зрения ООП вы уже инкапсулировали знания о наличии свойства status в принципе, не говоря уже о том, какое значение отвечает за закрытое состояние.


Ну и скорее всего в Invoice у вас будет свойство invoices типа InvoiceItem[], для которого тупые сеетеры по идее должны быть вообще исключены, а общение с ними клиента класса Invoice исключительно через методы типа addInvoiceItem(Product $product, int $amount) и т. п., которые, скорее всего, будут не просто делегировать вызовы работы с массивом, а будут, например, проверять итемы на валидность, вести накопительные итоги и т. п.

Ок, спасибо за терпеливые разъяснения, буду обдумывать.
Сходить во внешний сервис — это не бизнес-логика, это простейшая координация действий. Не происходит изменения стейта, нет риска нарушения инвариантов, это простое чтение данных. Этим может заниматься сервис, для этого нам не нужна наша сущность, и поэтому анемичной она не станет. Если результат из сервиса нужен для логики сущности — вызовите метод сущности передав туда результат который вернул сервис.


Мне кажется ответ базируется на удобном интерпретирование вопроса. Ходить во внешний сервис — нет это не бизнес-логика, поэтому и о HTTP клиенте сущность не будет знать. Но как быть если все таки существуют инварианты для подсчета времени доставки? Что если время доставки зависит от статуса заказа (на складе, запакован уже и тд)? Или от других данных состояния: тип, размер? Выносить все эти проверки в сервис, где считается время доставки и устанавливается в сущность? Сущность все такая же анемисная, и метод `delivery` все тот же «сеттер» устанавлиющий несколько свойств сущности (проверяет часть инвариантов, но не все связанные с поведением доставки).

Если сервис действительно необходим сущности, то для сервиса создаётся интерфейс, инстанс сервиса (с заинжекченным, например, http клиента) передаётся сущности как аргумент методов, которым он нужен.


Но вот в данном кейсе скорее всего лучше будет сделать DeleveryService, который будет работать с сущностью Order через методы типа startDelivery(\Datetime $startTime, \DateInterval $plannedDuration), finishDelivery($finishTime), cancelDelivery($cancelTime) и т. п. При этом сущность будет отвечать за свои инварианты, типа возможности начать, закончить или отменить доставку из текущего состояния (неоплаченный заказ нельзя доставлять или нельзя отменить доставку, которая ещё не началась), соблюдения последовательности таймстампов и т. п. И у меня уже язык не повернётся назвать её анемичной.

Не полностью переезжает, а только та часть, которая сущности не касается. Сервис становится чем-то вроде декоратора к сущности.

А таким образом не теряется cohesion? Поведение, решающее одну задачу не становится как бы «размазанным» по нескольким классам?

Если взять пример в checkStatus выше, то определённо не теряет. Сущность инвойса отвечает за свой статус и его изменение, а сервис сихронизации инвойса отвечает за синхронизацию сущности с событиями во внешней системе. Сущность инвойса знает как себя закрыть, а сервис синхронизации и, вероятно, другие сервисы знают когда надо инвойс закрывать.

Ладно, я подумаю над этим, может действительно надо поменьше на свои сущности возлагать. Что-то в этом есть.

Эванс, если не ошибаюсь. Суть в чём: сущность длительно живущий объект, создающийся только один раз, а потом лишь изменяющийся. Ну и, может быть, когда-то уничтожается.

У него в 'blue book' ничего не говорится о том, что в сущности нельзя внедрять зависимости и вообще о конструктора, и внедрении зависимостей.

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


Но Вернон точно писал о том, что ужасно внедрять зависимости в сущности и VO в принципе. За исключением, конечно, других сущностей и ВО, но тут сложно говорить о зависимости, это часть их стейта по сути.

Как варианты решения первой задачи (единственно правильного нет):


  • геттеры (в "чистых" rich моделях сущностей часто без префикс get, типа order->status()
  • единый DTO (для PHP можно массив)
  • DTO под юзкейс (большое смешение оиветственностей часто получается типа orderForRegularUser и orderFirAdminUser)
  • адаптеры или view model, в том числе с рефлексией или подобными "хаками" или использованием одного из предыдущих способов

Вообще хорошим (но не всегда практичным) способом считается полная изоляция сущностей от вью. В MVC подобных приложениях модель для контроллера — сервисы, принимающие/возвращающие DTO или примитивные значения в более-менее удобном для вью виде, о сущностях контроллер не знает ничего.


По второму: по умолчанию, если надо изменять две сущности синхронно и/или зависимо, то этим занимается какой-то сервис модели.


Иногда практично передать вторую сущность или сервис как параметер в метод первой.


Классический DI в сущностях обычно кричит об очень неудачном распределении ответственностей в модели в целом.

единый DTO (для PHP можно массив)

Лучше это назвать CQRS. DTO и особенно массивы в большинстве случаев, по-моему, плохая и распространенная практика сокрытия аргументов функции, а CQRS лучше объясняет, когда подобная идея может успешно применяться. В read моделях и read логика кое-какая может быть.

CQRS отдельная тема. Я про представление сущности для view. Объект сущности уже есть в памяти процесса, надо какие-то данные передать во view, например, новое состояние после изменения. По CQRS на query операции нам и сущности-то не нужны.

я не могу отделаться от ощущения что в общем случае при каких-то сценариях реализация этих методов для работы с данными entity может потребовать использования какого-либо внешнего объекта. В этом случае перед нами встаёт вопрос dependency injection для entity.
Можно посмотреть такое понятие, как aggregate root. То есть создавать сущности, которые уже содержат другие сущности.
Но если речь об общении между доменами (границы между которыми определяет разработчик), и тем более об общении с внешним миром, то это уровень сервиса приложения.
Чтобы говорить конкретнее, то лучше всего оперировать конкретным бизнес-сценарием.
По первому вопросу, зачем вам сущность для отображения? Обычный SQL и результат в DTO или в массив и на вывод пользователю (ReadModel)
По второму вопросу, нужно больше деталей, т.к. все оч сильно зависит от кейса, но в любом случае другой объект можно передать как аргумент в сам метод, и не нужно никакого DI, но опять же все оч сильно зависит от задачи, и как спроектирован домен.
Большое спасибо за ссылку и вопросы. После некоторых размышлений, пришел к выводу,
что сущности entity должны включать в себя какие-то собственные методы типа toArray для сериализации. Причем, какого-то красивого решения я таки не выбрал, ввиду того, что каждый должен сам знать, как ему себя сериализовать (и зачастую, это получается индивидуально у каждой сущности).

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


Я пока не решился уйти от getter'ов, поскольку во множестве сценариев они упрощают код, не принуждая создавать конвертеры в DTO на каждый чих. С практической точки зрения бывает необходимо получить какое-то единичное значение из entity, например при построении запросов на выборку с условиями. Здесь условия могут быть очень разными, соответственно и поля могут потребоваться любые, а задействовать полное преобразование entity в DTO или даже в массив — дополнительное время, ведь большинство сделанной работы будет выкинуто. К тому же в случае массива мы теряем поддержку типов и autocomplete, если только не описывать shape возвращаемых данных через аннотации.


Избавление от setter'ов и переход на статические методы для создания + методы изменения внутреннего состояния объектов делают код более надёжным, однозначно.


Отдельно хочу отметить появление в этом случае проблемы свяанных entities. К примеру если у нас есть Product и Detail со связью 1:N через Product::$details, то очевидно, что для создания Detail нам нужно знать Product, а затем ещё как-то надо поместить новую entity в Product::$details.


Это приводит либо к необходимости:


  • либо иметь в Product метод, добавляющий Detail в Product::$details с вызовом $product->addDetail($instance) в Detail::create(). Работает, но возникает проблема если новую entity не надо помещать в коллекцию или если Product для этого Detail не определён вообще.
  • либо иметь setter метод в Detail, привязывающий Product при добавлении в коллекцию.

После экспериментов я остановился на следующей схеме: В Detail::create аргумент Product не передаётся, взамен этого добавляется метод:


/**
 * @internal 
 */
public function withProduct(Product $product): self
{
  $this->product = $product;
  return $this;
}

Метод отмечен как @internal через аннотации. К сожалению friend class в PHP не поддерживается, поэтому ограничение доступа на уровне соглашения, а не языка. В сам Product добавляется метод:


public function addDetail(Detail $detail) 
{
  $this->details->add($detail->withProduct($this));
}

Эта схема позволяет решить описанную выше проблему не усложняя код.


Помимо этого стоит отметить ещё один момент, не описанный в статье: коллекции. Очевидно, что getter для коллекции может вернуть Doctrine\ORM\PersistentCollection со всеми её возможностями по модификации её содержимого что возвращает нас к вопросу о запрете прямого изменения внутреннего состояния entity. Чтобы не допустить этого имеет смысл реализовывать такой getter примерно вот так:


public function getDetails(): Collection
{
  return new ArrayCollection($this->details->toArray());
}

В таком случае возможности воздействовать на внутреннее состояние не через методы entity уже не останется.

не согласен принципиально

Если у нас не CRUD приложение, где вообще можно публичными отделаться, то есть Entity номинально воплощают простые ДТО — тут согласен

А если это полноценный бизнес-объект, то появление геттеров провоцирует работать с этими данными там, где не место бизнес-логике, со временем провоцирует появление логики не там где нужно, появляется соблазн поместить еще больше данных «логически связанных с имеющимися», то размывает ответственность и усложняет код.

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

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

Вот и неверно понимают, инкапсуляция это немного другое:
Инкапсуляция — упаковка данных и функций в единый компонент

Инкапсуляция – это концепция сокрытия всего личного. Чтобы каждый объект как организм жил своей собственной жизнью.

Автор сам придумал себе проблему вынеся логику в модели. Да модели следует рассматривать как простые DAO. Это механиз мапинга ваших данных в БД. Не более того. Всю логику при надо вынести отдельно к примеру в репозитории, контроллеры и т.д., а чтобы защитить себя от случайного использования модели напрямую, можно написать доктрин листенер, который будет проверять к примеру какой-то флаг у энтити к примеру isExpectedUpdate перед изменением. Этот флаг будет устанавливать ваш код бизнес логики, и если он false, листенер ругается и отменит flush. И не надо морочить голову с инкапсуляцией.


Если бы я увидел логику в слое ORM отличную от computed properties, отправил бы это на переделку

Механизм маппинга — это как раз ORM, доктрина в данном случае. Сущности и репозитории слою ORM не принадлежат, они вход/выход для него.

Как раз таки бизнес-логики не должно быть в контроллерах. Плавали, знаем...

Хорошо представим что в методе isDelivered потребовалось что-то кроме полей самой модели, что тогда делать? Инджектить как-то зависимость в модель? Передавать зависимость постоянно в метод isDelivered($someService). Это неправильно. Есть репозитории в которые как нормальные сервисы можно инжектить зависимости. Автор отвечает что, а зачем туда зависимости, модель только за свои данне отвечает. Ну и получится в итоге что либо isDelivered не может быть вычеслен на уровне модели в принципе, либо isDelivered на самом деле не является тем чем он есть (не отвечает в полной мере на вопрос так доставлен же товар или нет). Ну т.е. в более сложном сценарии isDelivered скорее всего сам по себе переедет куда-то в другое место.

И как автор к приеру выбирает из базы данных все заказы которые delivered? Пишет что-то вроде SELECT FROM WHERE status = «delivered», все равно логика уехала из модели. И скорее всего будет перемещена в репозиторий. Ну так зачем размазывать логику везде, уже пиши все в репозитори.

А потом автор прикрутит какой-то сериалзитаор и он ему на 1000 моделях понадергает публичные методы delivered т.д. и хорошо если это просот сильно замедлит скрипт, хуже если эти публичные методы приведут к каким-то side effects нежелательным.

Еще автор сам говорит что некоторые поля нужны (для DTO), а некоторые нет. Происходит совмещение понятий и что всегда не есть хорошо. И вообще в современном мире когда все взаимодействие идет с бекендом обычно через АПИ, вообще вопрос инкапсуляции решается не сеттерами и геттерами, а группами сериализации, которые решают что видно извне и формируют интерфейс взаимодействия.

И если уж автор решил заняться инкапсуляцией, тогда я ему надо посмотреть в сторону паттернов Command либо Workflow. Грубо говоря отдельный слой, для работы с моделями, а с самими моделями напрямую не работать вообще.

А в подходе автора получается ни туда ни сюда. Вроде инкапсуляция есть, но не в том месте где она нужна.
Хорошо представим что в методе isDelivered потребовалось что-то кроме полей самой модели, что тогда делать? Инджектить как-то зависимость в модель? Передавать зависимость постоянно в метод isDelivered($someService).

Если состояние «доставлено» перестало быть обычным флагом, то, возможно, стоит задуматься о выделении этого состояния в отдельный класс. Вполне может получиться, что «доставка» — это отдельная сущность со своей логикой. Это сильно зависит от контекста.


И как автор к приеру выбирает из базы данных все заказы которые delivered? Пишет что-то вроде SELECT FROM WHERE status = «delivered», все равно логика уехала из модели. И скорее всего будет перемещена в репозиторий. Ну так зачем размазывать логику везде, уже пиши все в репозитории.

Есть мнение, что сущности и логика в них предназначены, главным образом, на запись. На чтение можно использовать обычные массивы, DTO и т.д. Я думаю, что в этом случае одно другому не мешает, т.е. логика никуда не уезжает.


А потом автор прикрутит какой-то сериализатор и он ему на 1000 моделях понадергает публичные методы delivered т.д. и хорошо если это просто сильно замедлит скрипт, хуже если эти публичные методы приведут к каким-то side effects нежелательным.

И вообще в современном мире когда все взаимодействие идет с бекендом обычно через АПИ, вообще вопрос инкапсуляции решается не сеттерами и геттерами, а группами сериализации, которые решают что видно извне и формируют интерфейс взаимодействия.

А вот это мне совсем не понятно. Зачем использовать сложные штуки, если можно использовать простые? Группы сериализации, конфиги для них и т.д., и т.п., вместо того, чтобы просто достать из БД необходимый набор данных в виде массива, например. Ну то есть я не понимаю, зачем для UI делать выборку сущностей.


И если уж автор решил заняться инкапсуляцией, тогда я ему надо посмотреть в сторону паттернов Command либо Workflow. Грубо говоря отдельный слой, для работы с моделями, а с самими моделями напрямую не работать вообще.

В конце концов кто-то всё равно должен работать с сущностями.

Если в сущности в методе isDelivered потребовалось что-то кроме полей самой сущностей и полей связанных с ней, то выход очевиден — вынести этот метод из сущности в сервис.


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

А я не согласен. Я бы подумал, может и в сущность зависимость добавить, например если эта зависимость используется в большинстве методов сущности, а в isDelivered() три четверти кода используют состояние сущности.
Тогда вы нарушите правило изоляции слоёв (clean architecture). Хотя я не совсем понимаю, какого рода зависимость вы хотите добавлять в сущность.
Нет, не нарушу. Посредством интерфейса можно внедрять что угодно. Например EventBus.
Да можно получать этот EventBus даже из глобального состояния, кто вам запретит. Нарушит это в том плане, что доменный слой (бизнес-логика) будет знать о слое приложения.
А можете объяснить, каковы последствия этого нарушения? Связь же можно сделать посредсвом интерфейса, это тоже плохо? К тому же помимо сущностей есть бизнес-сервисы, это тоже часть БЛ, но в них зависимости все внедряют, в т.ч. и EventBus. Это разве не будет нарушением?
Посмотрите мой комментарий ниже, чтобы разговаривать об одном.

Я не знаю, о каких конкретно бизнес-сервисах вы говорите. Есть Application Services, есть Domain Services, и Event Bus, бывает, внедряется в первые, а лучше и сразу в инфраструктурный Persistence (в зависимости от роли). Могу представить в вырожденном случае даже использование этого паттерна в одной доменной сущности. Это всего лишь способ коммуникации внутри приложения, альтернативный обычному вызову "object.method(...arguments)".
Под бизнес-сервисами имею ввиду Domain Services. Если следовать логике, что домен не должен ничего знать о приложении, то тогда внедрение зависимостей типа EventBus в Domain Services будет нарушением правил чистой архитектуры? В доменные сервисы много раз видел как внедряются репозитории, это нарушение?
Это Application Service, стало быть. Application восстанавливает домен в памяти (путём получения необходимых данных), отдаёт ему команды, получает от него события об изменениях состояния и решает, что с ними делать (в основном сохранить и/или разослать).
С EventBus разобрались, все кто ее используют это должны быть в Application слое. А где тогда должна быть бизнес логика, которая юзает зависимости, например, если при рассчете времени доставки заказа нужно опросить внешнее API? В приложении этот API предположим, представлен как DeliveryService внутри которого http клиент. Можно ли его внедрять в доменные сервисы?

И все таки я еще про внедрение репозиториев в доменные сервисы хотел уточнить? Можно ли их туда внедрять или нет? Если нет, что тогда могут доменные сервисы? Если да — почему тогда нельзя внедрять репы в сущности?
Если нет, что тогда могут доменные сервисы?
Изменять состояние соответствующих доменных сущностей в соответствии с некоторыми инвариантами.
почему тогда нельзя внедрять репы в сущности?
Тут мой комментарий, вот комментарий EvgeniiR. Это касается сущностей. Я настаиваю на строгом определении и касательно сервисов (код может коммуницировать только с «поддоменами»), чтобы не размывать границы слоёв, но кто-то может использовать «доменный сервис» более широко. Но сущности — однозначно последняя точка «дерева» выполнения приложения.
А где тогда должна быть бизнес логика, которая юзает зависимости, например, если при расчете времени доставки заказа нужно опросить внешнее API?
В сервисе приложения для этого юзкейса. Сервис может быть с логикой для выяснения, откуда брать это deliveryTime (по возможности спросив у сущности), а не только тонкой прослойкой. Все данные для изменений состояния должны быть у сущности или в команде.
Я вас понял, спасибо. В общем в сущностях у нас данные и код для соблюдения инвариантов (согласованноси данных). В сервисах у нас все остальное поведение, включая обработку событий сущностей.
Мне это не близко. Напоминает процедурную декомпозицию: сущности — структуры данных (пусть и с инвариантами), сервисы — поведение. Это юзабельно, но мне не заходит, как то слишком мало при таком подходе могут сущности, мало инкапсулируют.

Резюмируя по теме статьи: получается, что если при использовании сеттеров инварианты не страдают, то ничего плохого в их использовании в сущности нет?

Код для соблюдения инвариантов в сущности — это свойство "правильной" сущности, по-моему, но не её цель. Сущность должна делать что-то полезное, кроме дачи гарантиий соблюдения инвариантов. А если она делает что-то полезное, то непонятно зачем сеттеры

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

Не нужны зависимости, по крайней мере те, которые обычно инжектятся через DiC чтобы делать что-то полезное. Ну вот в примере с инвойсом тут, методы isClosed и close вполне себе полезные для сервиса или другой сущности, решающих когда инвойс должен быть закрыт

если при использовании сеттеров инварианты не страдают
Инварианты не страдают, потому что они прописываются в ТЗ. В вашем подходе получается, что в одном месте инварианты сущности и инварианты приложения.
как то слишком мало при таком подходе могут сущности, мало инкапсулируют
Максимальная инкапсуляция — у God Object. Не знаю таких целей в разработке, как достигнуть расчетной инкапсуляции. Только лишь взять на себя то, за что отвечаешь и избавиться от лишних знаний.
Принцип «толстая модель» — это что-то из нулевых, по-моему. Но чуть позже в качестве «хороших» практик распространилась слоистая архитектура, в которой логика внутренних слоёв не зависит от внешних.

Вы выше предполагаете, что в вашей логике могут понадобиться понадобиться данные извне для каких-то решений. Это по сути требование репозитория. У меня окончательно сформировался к вам вопрос: вы считаете нормальным внедрять репозитории в сущность, когда бизнес-логика требует каких-то посторонних для сущности данных?
Что насчет внедрения других команд к другим доменам (через интерфейс, конечно), которые несомненно могут быть вызываться по сложным условиям бизнес-логики со сложными аргументами?
Вызов entity.command(arguments) принципиально может заключать в себе всю логику и вызываться около входной точки приложения? И сущность состоит из ряда таких методов?

Или часть бизнес-логики уровня приложения можно отправить в сущность, часть в вызывающий сервис в зависимости от оценочного понимания, где и что будет лучше?
Максимальная инкапсуляция — у God Object. Не знаю таких целей в разработке, как достигнуть расчетной инкапсуляции.

Я знаю, я нигде и не говорил, что должен быть God Ocject. Зачем вы его сюда примешиваете? В моем заминусованном примере Invoice не God Object. Инкапсуляция данных и поведения в объектах должна быть, иначе это не ООП, а процедурное программирование со структурами данных.

Инварианты никогда не страдают, потому что они прописываются в ТЗ.

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

вы считаете нормальным внедрять репозитории в сущность, когда бизнес-логика требует каких-то посторонних для сущности данных?

Да, я так считаю. Не понимаю, почему так нельзя, никаких конкретных примеров плохих последствиий такого решения мне никто не привел, все больше «Это плохо потому что так написано там то, это какие-то новые пути для ошибок, побочные эффекты», неубедительно.

entity.command(arguments)

Да я считаю, что в контроллерах можно так делать. Достать сущность, вызвать метод, показать результат выполнения.
Я знаю, я нигде и не говорил, что должен быть God Object.
Достать сущность, вызвать метод, показать результат выполнения.
Это как раз и получается God Object (сущность) — одна модель является несколькими кусками приложения. Лучше уж тогда разделять на сервисы, которые работают с чистыми структурами (чистый процедурный стиль), чем так, по-моему.
все больше «Это плохо потому что так написано там то, это какие-то новые пути для ошибок, побочные эффекты», неубедительно.
Как хотите, разумеется. Я лишь предлагаю решение для борьбы со сложностью. Если вы по какой-то причине со сложностью не боретесь, то моих ораторских данных не хватит вас убедить начать бороться. Это вообще tacit knowledge, мне тоже когда-то бесполезно было это говорить, да и сейчас многое бесполезно — просто не пойму проблемы.
Ладно, спасибо и на этом.
Это как раз и получается God Object (сущность) — одна модель является несколькими кусками приложения.

Нет, не будет это God Object, так как God Object все делает сам, а если есть делегирование другим объектам, то это нормальная инкапсуляция. Если следовать вашей логике, то все фронт-контроллеры это God Object, так как под Application::run() скрывается работа всего приложения.
В моем примере god object нет.
Если вы по какой-то причине со сложностью не боретесь, то моих ораторских данных не хватит вас убедить начать бороться.

Я борюсь со сложностью, с чето вы взяли что нет? Только я пытаюсь бороться объектной декомпозицией, а вы предлагаете процедурную.
Это вообще tacit knowledge, мне тоже когда-то бесполезно было это говорить, да и сейчас многое бесполезно — просто не пойму проблемы.

Так может просто нет никаких весомых причин не внедрять зависимости в сущности, вы об этом не думали?
Но сущности — однозначно последняя точка «дерева» выполнения приложения.

Скорее не последняя точка, а самые глубокие точки по дереву.

Я бы сделал интерфейс DeliveryService в доменной области, от которого зависят (по ситуации) или доменный сервис, или конкретный метод сущности. По умолчанию скорее первый вариант. И какой-то адаптер который существующий DeliveryService приводит к моему интерфейсу. Если существующий DeliveryService под моим полным контролём, то он будет имплементировать интерфейс. Если нет — введу адаптер, который будет имплементировать и получать существующий как зависимость.

Репозитории, вернее их интерфейсы — это специфичный бизнес-сервис по сути. У меня в коде они обычно лежат вместе с сущностью, класс User и интерфейс UserRepository определенно будут в одном неймспейсе, что типа App\Domain\User. А вот какой-нибудь DoctrineUserRepository или RedisUserRepository определённо будут в совсем другом неймспейсе, вне домена App\Infra\Persist

Возможно, вам лучше использовать паттерн Active Record (Laravel Eloquent, Yii) и не мучаться с Doctrine. Многие решения, примененные в ней, направлены на совсем иной стиль разработки. В отличие от Active Record.
1. Статья не относится к symfony
2. Статья вводит в заблуждение новичков.
3. Статья «ниочем»

Автору желаю скорейшего просветления и рекомендую к прочтению книги Мэтт Зэнсдтра PHP Объекты и шаблоны, документацию по doctrine2, документацию по symfony 4+.
Без обид.

Документация даёт самые простые примеры, чтобы в API библиотеки или фреймворка можно было быстрее въехать. Потому обычно не навязываются подходы к его использованию. Так что не стоит принимать примеры из документации за единственно верный способ программирования с использованием предлагаемого API.

У основного автора доктрины есть претензии к документации симфони по использованию доктрины.

Выше уже рекомендовали видео Marco Pivetta (Doctrine), прошу посмотреть:
youtu.be/WW2qPKukoZY?t=962

Это мой ответный «совет» :) Без обид :)

Посмотрел, и не понял причем тут ваша статья и лучшие практики в доктрине.
Ваша статья по примерам кода также никак не относится не к symfony, ни к doctrine.
У вас в примерах даже нет аннотации к классам сущностей.
Например Order у вас просто PPO, без определения мета информации.
Order класс не будет работать в доктрине.
Название вашей статьи противоречит свойствам языка PHP (публичные геттеры и сеттеры являются неотъемлемой частью сокрытия данных и реализации).

Документация даёт самые простые примеры, чтобы в API библиотеки или фреймворка можно было быстрее въехать

Вы явно не читали документацию.

У основного автора доктрины есть претензии к документации симфони по использованию доктрины.

Как это относится к статье? Вы сами прочитайте что пишите, «претензии к документации», а что в статье??!
Статья и видео — это два разных материала. Мы обсуждаем статью автора.
Тоже самое про статью от Marco, там никакого отношения нет к материалу автора.
Там описываются совершенно другие проблемы.
У вас в примерах даже нет аннотации к классам сущностей.

Аннотации — один из ЧЕТЫРЕХ способов задать конфигурацию для маппинга на сущность (аннотации, xml, yaml, php конифгурация)
В примере под спойлером есть аннотации

Не знаю зачем я вам привел пример с Марко, наверное показать, что тема, поднятая в топике — за пределами документации, на изучение которой вы меня толкаете :) Что как бы подразумевает ваши намерения, вот вам и скинул — тоже пообразовывайтесь.

Вы явно не читали документацию.

Вы в ответе обращаетесь сразу к нескольким людям.
У вас к Order не указаны аннотации (или любой другой способ задания маппинга), при этом вы используйте его как основной пример, а Project скрывайте, тем самым создаете путаницу.

При этом в Project вы опускайте работу доктрины и ORM паттерна — unitofwork будет следить за полями, и те не заполненные корректно поля вызовут ошибку вставки данных, обновления данных.

То есть конецпция сеттеров и геттеров как раз таки работает на типах, если у вас null вернется в mehod(): string, то вы получайте ошибку, если setter принимает тип, то другой тип вы не сможете туда просетить, сам php уже валидирует за вас данные.
Достаточно написать очень простой тест на объект на set и get и понять что UnitOfWork не будет класть туда null.
То что вы можете где то вызвать setter, а где то его НЕ вызывать, это валидируется внутри схемы через nullable. Мало того, ваше MVC обязано контроллировать входные данные. Ваш запрос должен проходить десериализацию и валидировать еще до того как попадет в слой данных.
Но мы глупые, мы решили опустить MVC, и сразу вводить пользовательские данные в БД.
Но и тут доктрина нас спасает, так как set и get типизированы.
То есть все условия с getter setter выполняются.
Вы же пишите
Тут у нас есть ряд not nullable полей — указаны как явно в аннотации и без указания (по умолчанию false). Вы точно на ревью увидите, что при создании объекта и наполнении его полями — все сеттеры на not nullable были вызваны, а тесты содержат все кейсы проверок? :)
Куда лучше бы было, если бы можно было создать объект одним методом — конструктором или именованным конструктором (например статическим методом). В них был бы четко заложены зависимости и инкапсулировано необходимое для создания объекта поведение.


UnitOfWork вам не даст воткнуть not nullable, как и схема в БД если при создании миграции разработчик целенаправлено не изменил схему, оставив ее в маппинге. При этом даже в этой ситуации unitofwork проверит в setter и если там придет null, он заругается.

При этом вам также никто не мешает создавать объект одним методом: через конструктор, больше, а сеттеры убирайте. У вас останутся только приватные свойства которые вы будете сетить в конструкторе.
И да автор, вы настолько не опытны, что даже не поняли почему класс Order не будет работать в доктрине. Для вас специально дополню: doctrine2 построит запрос не экранируя order -> тем самым вы будете получать постоянную ошибку в SQL.
Поэтому к нему важны аннотации. А вот к Project можно опустить аннотации.
ок, простите, что не подумал про вас, и не добавил описаний, что означает слово «пример», куда именно смотреть на код и как написанное интерпретировать

давайте оставим беседу с вами

я пойду опыта набираться, вы перестанете минусы получать от «ужасных» и неумелых хабравчан

либо + 10 баллов вам за троллинг
(публичные геттеры и сеттеры являются неотъемлемой частью сокрытия данных и реализации).

Заблуждение, по-моему. Сами слова "геттеры" и "сеттеры" обычно оззначают, что они практически предоставляют прямой доступ к внутренним свойствам объекта. Формально они защищены, да, но по "ментальному конвешену" рни просто акцессоры к свойствам, пускай и дающие какие-то гарантии типа, что статус меняется исключительно по какому-то флоу, из любого в любой перевести нельзя, но вы, наверное, очень удивитесь увидев в реализации метода User::setStatus() обращение к внешнему сервису и не увидев свойства $user. Ну или протсо вэтом методе не только присвоение значения внутреннему полю, но и, например, изменение значения кучи других свойств, если параметер метода "ACTIVE"

Вам сударь пример.
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
/**
 * Class Order
 * @package App\Entity
 * @ORM\Entity
 */
class UserOrder
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * Order constructor.
     *
     * @param $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }

    /**
     * @return integer
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

}

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

постыдились бы своих слов.
мы воюем с вами на разных полях

зачем вы тащите сюда в комментарии примеры, как можно заммапить Доктрину на сущност (кстати, приводя при этом ОДИН!!! пример из четырех возможных)
кроме того в вашем примере явно сеттеры и геттеры лишние, тк дата мапперу им начхать

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

Потому что в вашем примере есть пример для Project и Order при этом только один из них относится к doctrine, и про него вы задаете «Риторический вопрос», а в Order не относится к доктрине — но при этом вы его обсуждайте :)
кстати, приводя при этом ОДИН!!! пример из четырех возможных

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

Это далеко от понимания вами моего кода.
В моем примере как раз таки нет сеттера и это как раз показывает что ваши «проблемы с инкапсуляцией» выдуманы вами. Также в моем примере показана инкапсуляция которая дает клиентскому коду только публичные методы доступа.

Я понял что вам статьи нужны ради галочки, а не ради решения реальных проблем.
Пишите побольше таких статье чтобы понять насколько вы глупы.
Также в моем примере показана инкапсуляция которая дает клиентскому коду только публичные методы доступа

Термин «инкапсуляция»(encapsulate — заключать внутрь чего-либо, в капсулу) говорит нам о том что данные и работа с ними должна происходить в одном месте. В вашем же коде состояние объекта можно вытащить наружу через геттер и работать с ним.
Вывод — нет в вашем коде инкапсуляции.
Товарищ что вы курите?)))
В моем объекте через геттер можно вытащить ТОЛЬКО значение, но ИЗМЕНИТЬ объект (состояние) нельзя.
Вывод — нет в вашем мозге способностей программировать на php.
вы глупы
да автор, вы настолько не опытны
Автору желаю скорейшего просветления
Для вас специально дополню
Вы явно не читали документацию.
Статья вводит в заблуждение новичков.
3. Статья «ниочем»

Дружище, извините меня за все, чем доставил вам неудобство…
Отныне не намерен читать ваши ответы и комментарии, вас не понимаю абсолютно, не намерен слушать ваши комментарии по части доки и прочих, тк повторюсь — тут обсуждение идет не работы Доктрины и Симфони. Остановитесь, дружище.

тут обсуждение идет не работы Доктрины и Симфони

У вас название статьи «проблемы с инкапсуляцией в Symfony проектах»
А тело статьи относится к доктрине :)
Извиняю вас — вы и сами скоро поймете все, а то
Сравнительно недавно работаю с Symfony (чуть более года)

это слишком мало чтобы разобраться на 100% в исходниках symfony и doctrine2
У вас название статьи «проблемы с инкапсуляцией в Symfony проектах» А тело статьи относится к доктрине :)

А не потому ли, что подавляющее большинство проектов на Symfony используют Doctrine в качестве ORM?

Браво — ваш вопрос еще один косяк статьи отражает.
Большинство могут использовать doctrine2 вне проектов на symfony.
Но проблема будет относится к symfony )))
Угомонитесь статьи-написаторы, вы сначала программировать научитесь, а потом уже статьи пишете, а не наоборот.
Ты втираешь мне какую-то дичь

Ну, а если серьезно, не поделитесь ли вы своим опытом? Просто стебаться и советовать «научиться программировать» — это, конечно весело, но бесполезно.

Э, что из того, что вы написали в коде противоречит моим словам? Сеттеров у вас вообще нет, а геттеры дают гарантии ожидаемых типов.

вы написали про заблуждение и прямой доступ к свойствам.
я привёл вам пример, где невозможно изменить состояние созданного обьекта. При этом есть сеттеры и геттеры.
сеттер в моем примере — это конструктор и он задаёт только начальное состояние.
Остальной текст в вашем комментарии совершенно оторван от контекста обсуждения статьи.

Причём тут возможно или нет. Ваш код подверждает мои слова, что есть некая очень распространённая ментальная модель, по которой у для каждого геттера/сеттера есть однименноё внутренне свойство у объекта. Она настолько распространена, что не то что в IDE, а даже в языки вводится её поддержка, хотя по факту это раскрытие данных о внутреннем устройстве объекта. И если с геттерами ещё не сильно удивишься, встретив не return $this->prop, то от для сеттеров как миниум ожидается исполнение контракта:


$obj->setProp(1);
assert($obj->getProp() === 1);

Это не модель, вы геттер и сеттер можете называть как угодно. Причём тут название методов?
setProp, может выставить вам что угодно, вы не знайте и не должны знать что внутри есть такое свойство. getProp аналогично. Название метода просто подсказка как работать с объектом. Если у объекта есть setStatus принимающий int, это не значит что внутри обьекта он выставит свойство status, он может делать там что угодно — вас это не должно касаться. Вам дали интерфейс доступа — причем тут внутр. свойства объекта?

Она настолько распространена, что не то что в IDE, а даже в языки вводится её поддержка
а вы спросите у JetBrains почему они реализовали в своих IDE функцию Code->Generate->Getter/Setters и он генерит по свойствам: не потому ли что очевидная реализация сгенерить методы по названиям свойство?!
Или вы предлагайте генерить рандмоные названия методов?
Откуда такое мнение я вообще не могу понять. Нигде нету пруфа что это именно сделано ради того что «распространено».
Или вы предлагайте генерить рандмоные названия методов?

Если бы вы прочитали пост, то поняли, что посыл — не делать такого рода (или сводить к минимуму) методы, которые только мутируют одно свойство вне контекста бизнес-логики, что провоцируют писать эту самую логику снаружу «где-то там», само существование атомарных мутаторов понижает внутреннюю связанность объекта (его cohesion) и полностью нарушает инкапсуляцию (что и заявлено в теме). И не зависит, как они называются.

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

Именно, очевидная, настолько очевидная, что она и в обратную сторону действует: подавляющее большинство разработчиков увидев методы get/setName будут уверены, что там внутри есть свойство $name, которое отдаётся геттером напрямую, ну а в сеттере могут быть проверки типа на пустую строку. И в подавляющем большинстве случаев они будут правы. Ещё иногда будет что-то вроде return $this->data['name'] в getName()

Если у объекта есть setStatus принимающий int, это не значит что внутри обьекта он выставит свойство status, он может делать там что угодно — вас это не должно касаться. Вам дали интерфейс доступа — причем тут внутр. свойства объекта?

Сеттеры/геттеры в рамках статьи рассматриваются исключительно как именно те, которые просто ставят, просто достают, то есть имитируют public свойство.
Такая практика, а именно жесткое ограничение сеттеров/геттеров только вышеописанным поведением, распространена, что и явилось причиной написания этой статьи.
1. В вашей «статье» нет слов «имитируют», «public свойство» и т.п.
2. Покажите мне пруф где описано в документациях к doctrine2 или к symfony что «такая практика, а именно жесткое ограничение сеттеров/геттеров только вышеописанным поведением, распространена», откуда вы это взяли?! Из своего горького опыта?!
В вашей статье нет ничего из вашего комментария выше.
Даже по словам предыдущего комментатора нигде нету на это намека.
Название статьи и ее содержание не соответствуют вашему комментарию.

В статье вы подводите итог: «Код с сеттерами/геттерами заметно усложняет код, со сложным доменом и так бывает не просто...». — простите что усложняет код, я вам привел пример UserOrder, что там осложняется то?

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

Внезапно. А вы хотили God Object — простыню в которой и работа с данными и валидация, и UI… Ах да это все виновата практика getter/setter когда IDE генерят методы автоматом.

Что же такое инкапсуляция? Под инкапсуляцией обычно понимают сокрытие данных, поведения, деталей и условий. Когда мы раскрываем детали

Вы вроде бы пишите что это сокрытие, а потом пытайтесь раскрывать. Где тут речь про «распространненную практику» ?!

Далее ваш пример с Order — у вас даже методы разные в нем но вы их сравнивайте, один с getterом, другой без него где есть deliver метод. Это разные логические сущности. Вы думайте active record паттерном а не ORM.
Тут в комментах многие писали вам про это, что у вас просто нет понимание для чего нужен ORM. И да это распространненая практика, потому что это ORM и это doctrine. И внезапно это конструкции языка без которых невозможно сделать что либо: )

При этом вы считайте что если Вася написал невалидный объект с невалидными сеттерами и контролем состояния, то это «проблема с инкапсуляцией в symfony проектах».

Удалите статью пожалуйста, не позорьтесь.

Вы думайте active record паттерном а не ORM

Это в вашей больной фантазии, в моих примерах ТОЛЬКО про инкапсуляуию бизнес-логики и БАНАЛЬНЕЙШЕЕ ООП

Active Record — это про хранение данных в БД, точнее умение модели это делать и о диком нарушении SRP. Где вы в посте увидели намеки на него, когда речь идет о бизнес-слое и бизнес-логике — остается только догадываться и призывать священника, чтобы объяснил эти дъявольские наречия ваших мыслей :)



К сожалению у вас нет понимания, как работает и Маппер, зачем он нужен и его роль, и даже что такое инкапсуляция вы не понимаете. Буквоедство вам простим :)
не знаю что такое «Маппер», погуглил не понял о чем вы.
Знаю только ORM, Маппер — это наверное из OSM тот кто карты вносит в БД, разве нет?
Active Record — не понял причем тут хранение. База данных хранит данные, а не active record. Модель не может хранить данные, модель только умеет получать к ним доступ.
Бизнес слой и бизнес логика — ага, и слово инкапсуляция, тоже очень совместимы.

DataMapper — нет, не слышали?

Причем тут DataMapper.
ORM — это техника
DataMapper — это шаблон.
Учитесь дальше. Изучайте что такое ORM и для чего он нужен.
ru.wikipedia.org/wiki/ORM
и посмотрите что ActiveRecord входит в список библиотек ORM, и непосредственно относится к реализации ORM техники.
Если автор даже не может понять что такое ActiveRecord, как он может учить других?!

ORM — это техника, да. ActiveRecord и DataMapper — самые популярные шаблоны реализации этой техники. Doctrine как ORM библиотека реализует DataMapper, Eloquent как ORM библиотека — ActiveRecord

В статье вы подводите итог: «Код с сеттерами/геттерами заметно усложняет код, со сложным доменом и так бывает не просто...». — простите что усложняет код, я вам привел пример UserOrder, что там осложняется то?

У вас как раз не тот подход, который критикуется в статье — у вас нет сеттера.


И да это распространненая практика, потому что это ORM и это doctrine.

Распространённая практика в Symfony+Doctrine проектах делать в сущностях пару сеттер/геттер на каждое поле, маппящееся на базу, чуть ли не автоматом (вроде даже генератор штатный так и делает). Автор Doctrine с этим подходом не согласен, кстати.


Внезапно. А вы хотили God Object

God Object получается если в класс сущности с геттерами/сеттерами добавить бизнес-логику. Если не добавлять, то язык не поворачивается называть это сущностью обычно, просто DTO какой-то. Эта статья про то, что лучше хотя бы сеттеры убрать, чем смешивать геттеры/сеттеры и бизнес-логику в одном классе.

И еще чтобы показать вам вашу глупость, вы даже не подумали «как я могу улучшить код», вместо этого вы осуждайте его.
Ниже приведу код, который уже поражен проблемами, которые появились из-за возможности нам «удобно» читать поля

Посмотрите на код. Его легко привести в зависимость от объекта, просто убрав логику в сервис. К тому же doctrine2 регламентирует использование репозиториев, а вам нужно еще управлять UI. Заказ не найден — это бизнес логика. Поэтому ничего в этом страшного нет.
$order = $this->orderRepository->find($orderId); 
if(empty($order)) { $this->createNotFoundException(); }
if ($order->canHandled()) {
 //тут можно даже указать объекту что он вошел в состояние обработки
  $this->orderDeliveryService->handle($order);  //или тут
}

К тому же вы не рассматривайте сложный случай, когда у вас будут независимые от заказа объекты и логика с ними, элементарно вам понадобится транзакция. И тогда вам нужно делать так:
$order = $this->orderRepository->find($orderId); 
if(empty($order)) { $this->createNotFoundException(); }
$this->orderRepositroy->beginTransaction();
$log = new SystemLog("new order");
try {
if ($order->canHandled()) {
 //тут можно даже указать объекту что он вошел в состояние обработки
  $this->orderDeliveryService->handle($order);  //внутри состояние модифицруется но мы раскроем его здесь
 //код реалзиации handle
 $order->makeOrderDelivered(); //аналог $order->setStatus(Order::STATUS_DELIVERED)
  $this->getOuterCustomerService()->log($log); //например внешний сервис который может не работать
  $this->orderRepository->commit();
 return $order; //Тут вам уже доступен объект с другим status - бизнес проверка состояния -> $order->isDelivered() или $order->getStatus() более глубокая проверка состояния для других нужд 
} 
} catch(\Exception $e) {
 $this->orderRepository->rollback();
}


Оперирование объектом удобнее, во всех планах.
Если вам не нравятся сеттеры/геттеры, то вас никто не принуждает их делать, нет такого требования в документациях. Вы определяйте public свойства в своем Order. Тогда код выше изменится на $order->status = Order::STATUS_DELIVERED. Но тем самым вы даете возможность любому сервису в любом месте, даже внутри repositroy->find изменить ваш объект.

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

Вывод: вы просто не смогли понять код и начали ругать всех и вся.

Речь о том, что не должно быть в коде, претнедующем на качество ни


 $order->status = Order::STATUS_DELIVERED;

ни


$order->setStatus(Order::STATUS_DELIVERED);

должно быть только что-то вроде вашего $order->makeOrderDelivered()


Я вот не пойму, вы с Maksclub говорите об одном и том же, просто не понимаете этого, или где-то кардинальные различия?

мы не говорим, тут идет монолог с обвинениями :)
По каким таким стандартам оценивайте «качество»?
Почему название метода влияет на качество?
Почему должно быть «ТОЛЬКО».
С чего такие ограничения?
Вы опять решили за всех и ввели свой стандарт качества, ничем не аргументированный и нигде не подтвержденный.
Автор видимо придется мне опубликовать рецензию на вашу статью.
Опубликуйте, было бы интересно! Серьезно, без троллинга.
Здравствуйте. Спасибо за хорошую статью!
Мне кажется, в первом примере проверка deliveryDate является лишней в контексте того, что мы изменяем это поле только в методе deliver.
А если объект оказывается в несогласованном состоянии (через рефлексию или данные в БД испорчены), то видимо правильней бросить исключение.
спасибо, пример чисто академический — именно для примера:
связать некие данные и инкапсулировать в одном методе (этих методов может быть больше конечно)
Окей, вы говорите, что сущности — это не тупые DTO и должны экспоузить наружу не просто геттеры и сеттеры, а методы, реализующие бизнес-логику и бизнес-правила. То есть быть умнее. Как быть с зависимостями? Например, представим, что Order::deliver() в вашем примере должен сходить ещё в какой-то сервис, что-то там проверить перед установкой статуса и/или отправить какое-нибудь уведомление после установки статуса. Как это реализовать, учитывая, что DI Symfony не даст вам воткнуть зависимость в сущность?

Ну так этот код остается в сервисном слое, просто теперь вы не вызываете сет методы, а вызываете 1 метод, который уже принимает все входные данные, и дальше сам занимается их установкой и проверкой.

Тогда получается, что Order::deliver() всё ещё инкапсулирует не всю необходимую логику, часть её всё ещё лежит в каком-то стороннем сервисе. И другой сервис всё ещё может вызывать напрямую Order::deliver() в обход первого сервиса. Это чуть лучше чем тупые сеттеры/геттеры, но всё ещё недостаточно хорошо. Принципиальная проблема инкапсуляции всей логики на уровне сущностей не решена.

Не стоит задачи инкапсулирования всей логики на уровне сущностей. Сущность инкапсулирует только то, что касается только её. Может эмитирует события наружу, но не более. В идеальном DDD мире :)

Тут вижу две проблемы:

1) Задача инкапсулирования всей логики всё равно стоит. И если вы (или DDD) предлагаете её решать не на уровне сущности, то где? На уровне сервисов? Тогда как защититься от того, что API сущности позволяет разным сервисам по-разному с ней работать? Это очень напоминает исходную ситуацию, когда в сущности только тупые геттеры и сеттеры, и мы защищаемся от бездумного дёргания этих методов лишь тем, что договариваемся, мол, с этой сущностью можно работать только через такой-то и такой-то сервис. То есть, эта защита на уровне договорённостей, а не языка. Понятно проблему сформулировал? Что нам говорит DDD делать в таких ситуациях?

2) Сущность User хочет инкапсулировать в себе генерацию пароля. Ей для этого нужен сервис, который генерит рандомные строчки. О том, что он ей нужен, знает только сама сущность, это деталь её реализации, и внешний код, который использует этот метод, по-хорошему не должен догадываться о том, что сущность использует этот сервис. DI Symfony не даст нам нормально воткнуть в сущность эту зависимость. Да и DDD говорит, что зависимости в сущностях — это атата. Как быть?

1) Полноценная сущность надёжно контролирует свои внутренности на своём уровне. Оркестрацией сущностей занимается внешний сервис-юзкейс. В отличие от имения набора сеттеров в сущности, с которыми другой внешний сервис может её испортить, забыв присвоить какое-нибудь поле.


2) Спокойно используется передача зависимостей в метод. Приходится протаскивать. Не синглтоны же в методе дёргать. Вместо передачи в конструктор, с которой у сущности как раз будет атата. У сущности не должно быть циклических зависимостей от внешних прикладных сервисов. А на своём доменном уровне может дёргать кого угодно.

1) Полноценная сущность надёжно контролирует свои внутренности на своём уровне. Оркестрацией сущностей занимается внешний сервис-юзкейс. В отличие от имения набора сеттеров в сущности, с которыми другой внешний сервис может её испортить, забыв присвоить какое-нибудь поле.

Ну да, лазеек для поломки сущности как будто бы становится сильно меньше. Но всё ещё остаются, если мы не можем передать нужные зависимости в метод. Если же можем, то, кажется, что API сущности становится полным и при этом единственным местом для работы с сущностью. Годится.

Не синглтоны же в методе дёргать.

Смотря что понимать под синглтонами в данном случае. Внешний код, которому придётся передать эту зависимость в метод, всё равно получит её из DI-контейнера, и она фактически будет точно таким же синглтоном (в том смысле, что это один и тот же инстанс на всё приложение). Если внешний код может получить эту зависимость из какого-то общего контекста (DI-контейнера), то почему мы не хотим, чтобы модель могла сама её получить оттуда же?
то почему мы не хотим, чтобы модель могла сама её получить оттуда же?

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


Есть ещё варианты, например использовать AOP через аннотации, который подменяет сигнатуру методов, добавляя ещё один параметр, значение которого берётся из DI — но это дорого в плане ресурсов и/или дев-флоу.

Я использую SL в конструкторе и там же делаю проверку на тип сервиса. Получается почти то же самое, что и настоящий конструктор, только с зависимосью от SL
class Order {

    private $dep;
    
    public function __construct()
   {
       $this->dep = ServiceLocator::instance()->get(Dependency::class);
       \webmozart\Assert::isInstanceOf(Dependency::class, $dep);
   }

}

Получается почти то же самое, что и настоящий конструктор, только с зависимосью от SL


Получается совершенно не то же самое.
Получается утеря над контролем зависимостей, новые виды и пути для возникновения ошибок.

И ещё — вы покрываете unit-тестами эти классы Мокаете все зависимости?
Да мокаю, здесь проблемы нет. Просто в setUp() локатор пересоздаю и нужные для теста моки туда добавляю. Можно и сам локатор замокать, с этим тоже проблем нет, но эта штука крайне простая, поэтому добавляю в него моки сервисов.
Получается утеря над контролем зависимостей, новые виды и пути для возникновения ошибок.

А можете более подробно разъяснить, в чем именно заключается утеря?
Какие вы видите новые пути для возникновения ошибок?

1) Состояние хранится в сущностях, логика работы с ним размазывается по сущностям и доменным сервисам (VO, репозитории и т. д. опустим для краткости). В отличии от тупых сеттеров мутирующие состояние бизнес-методы сущности дают гарантии, что состояние сущности всегда соответствует бизнес-правилам, значения свойств согласовано. Работать с сущностью может любой сервис по умолчанию, гарантии даются на уровне кода. Средства языка бизнес-гарантии не дают.


2) "Кто ж его посадит, он же памятник" В смысле мало ли что сущность хочет :)
Если серьёзно, то DDD-подходы это не серебряная пуля, по умолчанию обычно подобные зависимости инжектятся через параметр нужного метода через интерфейс, $user->generateNewPassword(IPasswordGenerator $generatorPassword). То есть с точки зрения сущности полностью на уровне непосредственного клиента конфигурируются. Он их, конечно, может получать из DIC, если сам сущностью не является. А, в принципе, как раз пароль один из хороших практических примеров, когда тупые сеттеры имеют право на жизнь — клиент и сам может генератор дернуть.

$user->generateNewPassword(IPasswordGenerator $generatorPassword)

Вроде рассуждаете много правильно, но в нескольких местах даёте вредные советы, по-моему. Тут нарушение слоистой архитектуры.
Книжка Эванса о DDD в основном о том, что такое ubiquitous language, как важно выделять бизнес сценарии, выделять границы доменов, события события, имеющие смысл для бизнеса и так далее.
Что тут получается, юзер генерирует свой пароль на сайте, это правильное описание? Тогда либо внешняя зависимость не нужна, либо это ответственность уровня приложения. Нет ничего плохого в логике на уровне приложения.
Вообще есть совет, не знаю насколько удачный, начинать с логики на сервисном уровне и потом переносить её на уровень домена, то, что связано с логикой изменения состояния агрегата.
А так лучше? $user->generateNewPassword(), зависимость уже внутри?
Если пользователь знает как генерировать пароль, то нормально. Зависимость тут не нужна. Если этот метод по сути кусок application layer, засунутый внутрь бизнес-сущности, то это против стандартных рекомендаций разработки.

Я тут конкретную задачу рассматривал — как передать зависимость в сущность абстрагируясь до юзера и пароля. И, приведя пример, увидел, что лучше как раз тут просто сеттер для пароля сделать, о чём тут же и написал.

Вроде рассуждаете много правильно, но в нескольких местах даёте вредные советы, по-моему. Тут нарушение слоистой архитектуры.
Книжка Эванса о DDD в основном о том, что такое ubiquitous language, как важно выделять бизнес сценарии, выделять границы доменов, события события, имеющие смысл для бизнеса и так далее.


В той же книги (или у Вернона, я уже не помню) приводится пример что репозиторий это доменный объект, но его имплементация это persistence. То же самое и с примером выше, с чего вы взяли что IPasswordGenerator это другой слой? Это объект домена User, котрый генерит свой пароль, именно User контролирует инварианты, когда его генерить и в каком сосотянии (например при регистрации). А уже имплементация IPasswordGenerator это уже не дело User, и он ничего об этом другом «слое» не знает, консумер `generateNewPassword` только знает и передает ту имплементацию какую хочет.
1) Конкретно всю логику сущность и не должна инкапсулировать. При $order->deliver() меняется внутреннее состояние самого $order, ему неважно что нужно юзеру смс отправить или баллы бонусные посчитать. При этом $order может возвращать связанные с ним события при вызове какого-то emitNewEvents(), которые родительский сервис может диспатчить или не диспатчить. Если речь о том что в любом месте кода можно достать $order и вызвать deliver(), то с этим ничего не сделаешь, тут тысячи способов сломать стейт, рефлексия, апдейт напрямую через pdo и т.п. Если бы в пхп можно было разбивать код на модули и делать приватны для модуля классы то может быть.

2) Генерация пароля это отличный кандидат на свой отдельный класс/сервис. Много вопросов как раз из-за того что это не обязанность юзера их генерить, а делать метод внутри юзера, который принимает сервис и делает что-то вроде $this->password = $service->generate() ничем не лучше обычного сеттера. Для безопасности сеттеров можно как раз использовать Value Objects, например GeneratedPassword.

А на основании чего решили, что это его обязанность?

А я еще ничего не решил, просто уточняю.
На основании чего так решили

На основании того что работы со стейтом объекта Order не происходит. Order вообще может не хранить email пользователя. Суть всей затеи в том чтобы инкапсулировать работу с состоянием.


Координация действий (доставить order => отправить email) может быть спокойно вынесена в сервисы, т.к. если в них нет ветвлений нет нужды покрывать их юнит тестами.
Альтернатива — ивенты. Меньше контроля(вся координация уже не в одном месте), но ниже связность системы.

Так речь не об Order, а о User и генерировании пароля. А так согласен, в ООП одним из критериев распределения поведения является использование в этом поведении стейта, грубо говоря первым какндидатом на поведение будет класс, у котого есть стейт, с которым работает это поведение.
Order вообще может не хранить email пользователя. Суть всей затеи в том чтобы инкапсулировать работу с состоянием.


Суть в том что Order имеет Customer и бизнес правило утвреждает что «при доставке Order нужно сообщить клиенту» или перефразировав «ордер при смене состоянии на доставленный должен сообщить клиенту».
При $order->deliver() меняется внутреннее состояние самого $order, ему неважно что нужно юзеру смс отправить или баллы бонусные посчитать.


Почему не важно? Если домен Order требует чтоб при смене состояния заказа на «отправленный» дать знать клиенту или посчитать бонусные балы клиента?

Баллы может считать и Customer, хотя попахивает, но вот отправлять сообщения точно отвественность ни Order, ни Customer. Order или DeliveryService эмитят событие типа OrderDeliveredToCustomer($orderId, $customerId), а какой-то CustomerNotificationService подписывается на него и при наступлении как-то уведомляет клиента (или персонал, что не получилось уведомить)

Бонусы наверняка тоже лучше в отдельную сущность вынести, т.к. логика там может быть самая разнообразная)

Ну полную модель и инфраструктуру в комментариях строить как-то не хочется. Ну если уж на то пошло, то подсчёт бонусов скорее сервисом будет с разными стратегиями, но может без сущностей.

А я и не говорил что именно Order считает бонусы, это может делать и Customer и бонусы могут быть отдельным классом и много чего может быть, сложно дезайнить классы не знаю требования и домен. И да, ивентытоже могут быть использованы если они используются в архитектуре. Вы не уловили мысль, скорее всего это моя вина, я не правильно выразился. Идея в том что Order ответственен при своей смене запустить процесс начисления бонусов, будь то вызов поведения Customer либо записать ивент и сделать это асинхронно. Это не дело какого-то там «Application Service»? Это логика домена.

Я считаю смысл «Application Service» в настоящее время не отражает изначальный смысл этого термина. Например возьмем историческую работу Жефри Палермо jeffreypalermo.com/2008/07/the-onion-architecture-part-1
В ней если разобраться Application layer и Application services это что-то очень общее для кординирования приложения в целом: это SessionManager, AuthenticationManager, CommandBus. Это не сервисы которые содержат бизнес логику, знаний как реагировать на смену ордера, нужно ли начислать бонусы или нет, отправлять письмо по смене заказа или нет, и тд. Все эти классы с одним методом «handle», часто встречающиеся в настоящее время, даже ООП нельзя назвать, это обычные функции и от объектов там нет ничего.

Вот я не считаю, что класс Order должен быть ответственен за начисление бонусов при смене статуса на "доставлен". Он ответственен, в "худшем" для него случае, за оповещение остального домена о смене статуса. А какой-нибудь сервис посчитает бонусы как реакцию на это событие, а то и как реакцию на отсутствие ошибки. Сервис доменного уровня, Domain Service, а не Application Service. Вот здесь поверхностно описано https://enterprisecraftsmanship.com/posts/domain-vs-application-services/


На практике часты разные мнения о том как именно (скорее на каком уровне) связывать событие типа смены статуса заказа и запуск процесса начисления бонусов. Я часто это делаю на уровне Application Service из соображений практичности, что не создавать отдельный Domain Service для подобных кейсов пока их мало.

Принимается. Технически это действительно сработает в большом количестве случаев.

Вижу два неприятных момента:

1) Symfony DI тут никак не поможет — зависимости придётся передавать явно. В частности, если у нас есть стек вызовов таких умных методов, и самому глубокому из этих методов понадобилась какая-то зависимость, её придётся пробрасывать через весь стек, не получится просто взять её из контейнера на нужном уровне.

2) Циклические зависимости. Слой сущностей начинает зависеть от слоя сервисов, который, в свою очередь, зависит от слоя сущностей. Это, как минимум, неаккуратно (и идёт в разрез, например, с чистой архитектурой Фаулера, да и DDD тоже).

Есть идеи, как преодолеть эти две неприятности?

2) Слой сервисов — абстракции

А можете подробнее? Непонятно, что именно имеете в виду и как это решает заявленную проблему.

1) Да, передаём внешнему методу весь комплект. Нам снаружи всё равно, сам он будет дело делать или приватным методам делегировать.


2) Например, метод сущности принимает интерфейс, лежащий рядом с сущностью.

1) Я в соседней ветке привёл пример, когда это выглядит не очень. Сущность User хочет генерить пароль для пользователя, и ей для этого нужен сервис, генерящий рандомные строки. Использование этого сервиса — это настолько внутренняя деталь реализации, что обязывать код, вызывающий метод генерации пароля, знать об этой детали и инстанциировать нужную зависимость, чтобы передать её в метод, кажется прям совсем грязным решением. Но, честно говоря, это выглядит не как проблема DI в целом, а как проблема конкретных реализаций этого паттерна (в частности, реализации в Symfony). Хотя, с другой стороны, DDD-ребята вроде как идеологически против зависимостей от сервисов в сущностях. Но тогда я не понимаю, как предлагается красиво решать описанную проблему.

2) Ага, это Dependency Inversion из SOLID. Принимается.

2) угу, в рантайме по факту циклическая зависимость есть, но на уровне "компайл-тайма" сущность и сервис начинают зависеть от одной абстракции

Ага, это Dependency Inversion из SOLID. Принимается.
Inversion Of Control параллелен обсуждаемой «слоистой» архитектуре. Он может быть и при полном отсутствии слоёв. Это два независимых метода борьбы со сложностью.
Как насчет принципиального отсутствия побочных эффектов у доменного слоя, к примеру? Идея ведь сделать доменный слой «конечным», а не пробрасывать через домен вызовы обратно к внешним слоям.
В доменный слой можно передать интерфейс репозитория, да любой внешний интерфейс. Вон выше один пользователь Event Bus предлагает передавать, тоже через интерфейс. Пароль, к примеру, и микросервис ведь может генерировать. А потом доменный объект пишет в Event Bus событие о том, что пароль сгенерирован. Всё это неприкрытая коммуникация домена и приложения с инфраструктурой. Работать это будет, но сопровождение из-за запутанной графовой структуры приложения, вероятно, будет не таким простым.
Добавлю еще, что в большинстве случаев никакой бизнес-логики собственно у сущностей и нет, и речь идёт о простейшем BREAD (CRUD). Если так, то лучше оставить такую анемичную сущность, чем делать её прокси к вышележащим слоям. BREAD — это нормально, если никаких инвариантов действительно нет. Заодно сразу видно, что их нет и куда и как добавить.
Symfony DI тут никак не поможет — зависимости придётся передавать явно. В частности, если у нас есть стек вызовов таких умных методов, и самому глубокому из этих методов понадобилась какая-то зависимость, её придётся пробрасывать через весь стек, не получится просто взять её из контейнера на нужном уровне.

Мне кажется, что появление "сквозной" зависимости говорит о том, что что-то пошло не так...

Во во, я тоже очень много об этом говорил, в т.ч. в комментах на хабре. Даже смешно, когда народ удивляется, что это во многих doctrine-проектах анемичные сущности. Без зависимостей полноценную логику туда не положить, вот и получаются классы с одними геттерами/сеттерами. Кто об этом знает, начинает выходить из положения путем чего-то вроде Order::deliver(Dependecy $dependency), но иногда (не всегда) это то же своего рода раскрытие деталей, нарушение инкапсуляции. Когда у нас есть объект заказа, нас не должно волновать, что он там и как будет проверять, заказ должен знать все о таких проверках сам, тогда будет инкапсуляция.

А что считать полноценной логикой, а что неполноценной?


Да, бывает, что вроде одна сущность с кучей атрибутов, которые вообще друг от друга не зависят, изменяются только в отдельных юзкейсах. Но это крайне редко, по-моему.

Полноценная логика это логика, которая решает некоторую возложенную задачу. Геттеры, сеттеры, конечно, тоже решают свою задачу, но она слишком рудиментарная, поэтому и тут уровень икапсуляции низкий. Должно быть что скрывать.

Часто, если сущности нужны зависимости, то что-то не то с возложенной на сущность задачей. В смысле не на сущность она должна быть возложена. Вот выше есть пример с генерацией пароля сущностью User. Если попытаться описать её ответственность, то получится что-то вроде "представляет пользователя системы с такими атрибутами: id, username, password. Обеспечивает предоставление и изменении информации для аутентификации, а также генерирует новый пароль при необходимости." Вот это "а также" намекает на то, что может и не входит генерация паролей в отвественность сущности.

Вы правильно рассуждаете, но я бы так мелко не дробил, если мелко раздробить, получится куча объектов, каждый из которых сам по себе ничего толком не может, и пользоваться кучей таких объектов неудобно. Пароль разве не входит в информацию для аутентификации? По моему отдельно «а также» для генерации хеша пароля не нужно. Генерирование хеша это деталь реализации по обработке информации для аутентификации, что входит в представленную обязанность класса User:
Обеспечивает предоставление и изменении информации для аутентификации


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

Речь не про генерирование хэша, речь про генерирование пароля, по которому потом хэш вычислится.

Обеспечивает предоставление и изменении информации для аутентификации

Я думаю и генерацию пароля тоже как часть этой обязанности можно рассматривать

А я так не думаю. Из CRUD тут только RU формально.

Я считаю, что генерация пароля входит в «обеспечение предоставления информации для аутентификации», вы считаете, что не входит. Какого-либо правила, позволяющего однозначно сказать, кто прав, у нас нет. Тупик. Поэтому и работаем как получается, но плохого в этом ничего нет, работа такая)

Вопрос терминологии. Как автор "обеспечение предоставления информации для аутентификации" я утверждаю, что это только R из CRUD

Статья видимо навеяна этой www.yegor256.com/2014/09/16/getters-and-setters-are-evil.html

В статье нет ни одной отсылки к информации от Егора, потому сравнивать автора статьи с ним — лишь показывать свое невежество. Тема не нова, и Егор Бугаенко(со всем уважением к нему) ничего нового не открыл, пересказывая старую информацию под хайповыми заголовками.
Концепты доменных моделей существовали ещё когда ни Хабра ни блога Егора и в помине не было.

Странно что вы увидели сравнение.

Тоже про это всегда говорю, что полноценными ООП-шными объектами должны быть и сущности. Но как-то тоже мало кто реально соглашается.

1)
Давайте попробуем «поломать» простой объект:

Теперь посмотрите на этот пример:

Объекты имеют совершенно разный функционал, как можно их сравнивать? Как вы в первом объекте получите продукт, дату?

2)
Вот пример модели из Sylius, геттеров и сеттеров полно. Считаете, команда, написавшие этот проект ничего не понимают в разработке?

3)
Покажите пример проекта на гитхабе, с достаточно большим функционалом, и спроектированного правильным, по вашему мнению, образом
1. Вы дальше носа не видите. В том коде пример инкапсуляции. Добавив метод/методы для получения данных, вы класс не поломаете.
— первый способ: добавить геттеры, но при условии, что данные нужны для дто, сериализация, передаче во вью. Если для принятия бизнес-решений — чем плохо описал в статье, без авторитетов — просто причтите и уже спорьте с аргументами по предмету
— второй способ: в репозитории сделать метод, который сразу заммапит на дто. Отдавать во view — задача не для сущности доменного слоя, а контроллера
Именно о таких моделях, как по ссылке я и говорю, да

2. Мне нечего тут ответить, в виду того, что последнее время вы, Юрий, давите некими авторитетами. Отмечу, что выше приметили, что сам Марко Пиветта (главный мантейнер доктрины) утверждает ровно те же тезисы, что и я, и не только он. Ну вопрос ответный: разработчики битриксом не разбираются в разработке?

3. Любой проект с архитектурой DDD, к сожалению… я не хотел поднимать эту тему, тк и без DDD можно писать аккуратный не сложный код. Сеттеры и геттеры нарушают банальные принципы ооп, а практика в больших проектах ужасна

чем ужасно — написал в статье

Сделайте fluent сеттеры приватными, если вам так удобно, и вызывайте их из публичных бизнес-методов :)

2) Кастомные личные проекты пишутся под себя под свои процессы, которые заранее известны. А универсальные модули тем и универсальны, что дают менять что угодно кому угодно и не смогут предусмотреть все юзкейсы в мире. Там не сможете просто так дописать код напрямую в сущность из vendor. Так что сравнивать не совсем корректно.


3) Например, магазин на Yii, доска объявлений на Laravel и менеджер проектов на Symfony. Все без сеттеров на реальном ООП с конструкторами и методами.

Все без сеттеров

Но с геттерами.

Но геттеры при таком подходе могут быть не тупыми прослойками для защиты приватных полей. И создаваться не для всех полей подряд.

главное по геттерам, чтобы они не участвовали в бизнес-логике, а были только для транспорта. Именно это я и проговорил в статье, и указал, что не беру в расчет транспортное их использование.

Например плохо:
$newValue = $order->getTotalPrice() - $discount;

или:
if ($order->getTotalPrice() < $limit && $order->getStatus() === 'anyStatus')


Это все проблемы — код работает с состоянием объекта, но где-то там (на практике — в миллионе мест) и вся логика распылена по системе, из-за чего плодится копипаста и баги



Кроме того геттеры можно и не использовать совсем, если маппить данные для транспорта/отображения на ДТО

По-моему, вполне можно использовать геттеры в бизнес-логике. Например, в сервисе скидок вполне допустимо обратиться к $order->getTotalPrice(), чтобы посчитать скидку. Даже если сейчас правило предоставления скидок простое типа "больше N — скидка P процентов", то скорее всего в будущем они будут усложняться и зависеть от множества значений, в том числе к order никакого отношения не имеющих.

возможно, в статье попытался обратить внимание на опасность такого подхода
нет, я ошибся… данные totalPrice можно и лучше всего передать через ДТО, для чего он собственно и придуман

DTO придуман для передачи данных между, как минимум, слоями приложения. Сущности и доменные сервисы — это один слой, им не нужно изолироваться друг от друга через DTO.

в сервисе скидок вполне допустимо обратиться к $order->getTotalPrice(), чтобы посчитать скидку
В сложных случаях можно сделать отдельный изменяемый Orders\Order с инвариантами, и неизменяемый Discounts\Order (приватный для домена на уровне соглашений) с необходимыми данными.
Хотя можно на том же уровне соглашений (ну или через интерфейс) представить, что тот изменяемый Orders\Order и есть Discounts\Order и не плодить сущности.
Еще можно представить Order как aggregate root и изменять скидками его состояние изнутри.

Был небольшой опыт работы с Sylius.


  1. Сущности, в случае недосмотра, легко приводятся в некорректное состояние, т.к. состоят из геттеров и сеттеров.
  2. Логика размазана по репозиториям и сервисам. Иногда приходилось писать много кода, чтобы собрать всё нужное в кучу.

В Doctrine части документации Symfony есть давно известная проблема: она демонстрирует технику интеграции симфони и доктрины, но воспринимается очень многими как образец правильной симфони-доктрин архитектуры.


А контроллеры тоже могут быть полноценными сервисами. :)

Спасибо за статью! В конце статьи, в примере с геттерами, не хватает контрпримера с логикой внутри сущности. Было бы отлично туда его добавить :)

Мне кажется, что данную проблему нужно решать по-другому. Диаметрально противоположно. Нужно скрыть сущность от всех и управлять ею только через сервисы. Если количество таких сервисов велико, то что-то не так с сущностью и она «много на себя берёт».
Мне кажется, что данную проблему нужно решать по-другому. Диаметрально противоположно. Нужно скрыть сущность от всех и управлять ею только через сервисы.

Какую проблему решают сервисы?


Обычно непонимание Рич моделей происходит из-за того что "как это логику в сущности? Там же тысячи строк кода будет?", и решается разбиением одной сущности на несколько cohesive классов и пониманием разницы между координацией (порядок действий никакой логики), которую можно вынести в сервисы, и реальной бизнес-логикой, которая отражает поведение системы и скрывает за собой изменение состояния, контролируя его валидность.


Проблемы БЛ в сервисах — бизнес правила не выражены в коде, нет контроля за зависимостями, состояние не изолированно и каждый новый сервис должен помнить все инварианты сущности чтобы не допустить невалидного состояния, в системе больше зависимостей и сложнее покрывать логику unit-тестами.
Формально — выше coupling, ниже cohesion.

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

Встречал подход, когда сущности предметной области маппились на базу не напрямую доктриной, а через сущности доктрины, но особого профита от добавления ещё одного уровня абстракции не заметил, в отличии от подобного подхода с Eloquent. В некоторых кейсах полезно, прежде всего в случае, если поле сущности содержит массив VO, а база типа MySQL, и с точки зрения базы эти VO — полноценные записи со своим id, но вот форсить такую модель на все сущности смысла не увидел.

Думаю, нужно подчеркнуть, что "рич модели" не подразумевают, что вся БЛ предметной области находится в сущностях. Она распределена между сущностями и сервисам. В сущностях то, что относится только к сущностям, а в сервисах взаимодействие между сущностями (про агрегаты замнём для ясности) и взаимодействие с внешним миром.

Это не диаметрально противоположное. Тут вопрос в терминах, что значит "управлять сущностью" прежде всего. Вызывать методы сущности только из сервисов возможно, а некоторые считают только так и правильно. Вопрос лишь будут это методы $order->setStatus('DELIVERED')->setDeleviredAt(new Datetime())->setDeleviredBy($currentUser) или $order->deliver(new Datetime(), $currentUser)

Пример:
Например есть некая логика, которая затрагивает 4 поля сущности на основе одного аргумента. (Например) два затереть, третий посчитать на основе пришедшего аргумента, четвертый минуснуть третий с проверкой, что он больше третьего или равен ему.

  • Имеем один нормальный метод с одним аргументом с инкапсуляцией в нем изменений объекта и всегда железный один инвариант сущности, он при вызове этого метода всегда одинаков. Точек отказа — 1. Легко тестировать
  • Ваша ситуация:
    — два аргумента в методе сервиса — сущность и аргумент
    — сразу 4 вызова сеттера для изменения объекта помимо самой логики подсчета (на основе геттеров), кроме того данные находятся совсем в другом объекте, не в сервисе. Кроме самой калькуляции еще и 4 места ввести не то и не туда, поправить не так. В добавок менеджер этот еще и в контейнере сидит скорее всего — зарегистрировать и пробросить туда, где его вызовите… Тестировать — нужно прокинуть сущность, замокать репозиторий. Если вы не прокидываете сущность, как вы фиксируете, что сущность находится в нужном инварианте? Если прокидываете — еще одна сложность


Множество точек, провоцирующий на процедурный ад. Как только появляется в системе несколько человек, срочность и прочие человеческие грехи — такая система превращается в паутину и очень увеличенную сложность.

Вопросы про тестирование — как тестируете такой менеджер-сервис?

UPD: Тут в комментарии хорошо отметили проблемы в том числе и в тестировании: habr.com/ru/post/469323/#comment_20687161
При разростании логики
В вашем случае растет число сервисов, ДТО вокруг этих сервисов, паутины этих цепочек вызово. Сервис от сервиса ничем не отличаются сходу кроме разных неймспейсов, похожих друг на друга.

В случае грамотного построения домена — сервисы присутствуют, но куда в меньших пределах. Появляются ValueObjects, которые инкапсулированы все в ту же БИЗНЕС-сущность и лежат рядом в одном домене и в которых уже инкапсулирована логика, которая касается их. В них распределена та логика, что раскидана у вас по сервисам, возможно они вызываются через корень самой сущности в некоторых сервисах, но число точек отказа ограничено, инварианты контролируются единым входом через сущность. Все также персистится доктриной.
Извиняюсь за атаку (показалась, что осталась недосказанность)
Не предлагается втягивать в сущность постороннюю логику, много логики…
Только та, что касается доменной сущности (вызов внутренних методов и внутренних свойств), не более… Пост как раз об этом, банальное ООП в содружестве с разумом и разумными сервисами доменной области.
Кроме того данные и поведение объекта хорошо связаны, эта внутренняя связанность (cohesion) — хороший задел для построения хорошей архитектуры
Только та, что касается доменной сущности (вызов внутренних методов и внутренних свойств)


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

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

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

Я в целом с вами согласен, но для меня есть одно препятствие (если говорить по sf и Doctrine ORM): для бизнес-логики нужны зависимости, почти всегда. Если в сущностях оставлять код работающий только с состоянием сущности и не использующий зависимости, то кода в сущности будет немного, отсюда и возможное вырождение в DTO с геттерами и сеттерами. Препятсвием является невозможность внедрения зависимостей в конструктор сущностей в Doctrine ORM. Поэтому не использую эту ORM по собственному желанию, больше по необходимости (если она уже есть в проекте).
У меня в голове похожее решение: слой тупых сущностей с геттерами/сеттерами, поверх них слой локализованных сервисов, предназначенных для работы с одной сущностью, поверх них слой более сложных сервисов (интеракторов, юзкейсов, you name it). Сущности в таком подходе — почти тупые DTO. Первый слой сервисов — это такой типа способ подружить сущности, необходимые им сервисы и DI. Второй слой сервисов — классические интеракторы. Ну, и всё это смазать соглашением (которое никак не выйдет контролировать на уровне средств языка) о том, что с сущностью может работать только её сервис из первого слоя.
У меня еще одно ршение — внеднять необходимые зависимости в сущности через SL, но в sf SL считается злом (я считаю что это неоправдано).
Ну, злобность service locator лично у меня никаких сомнений не вызывает :—)
Сущности в таком подходе — почти тупые DTO.

Так назовите их DTO и не путайте людей :)


поверх них слой локализованных сервисов, предназначенных для работы с одной сущностью

А вот их назовите сущностями.

Переименование в DTO действительно уменьшит путаницу, а вот переименование обсуждаемых сервисов в сущности, скорее, наоборот. Но терминологию стоит подрихтовать, я согласен.

Если сервис работает исключительно с одним DTO, то точно ли он сервис с одной стороны, а с другой, нужно ли тут ООП вообще? Опуская, естественно, контекст PHP, где с функциями работать неудобно из-за отсуствия автозагрузки.

Да, это безусловно попахивает процедурщиной. Данные — в DTO (она же тупая сущность), операции над ними — в этом недосервисе (только что обсуждали этот подход с коллегами, родилось название «агент», чтобы отличать от полноценных сервисов). И всё это не из идеологических соображений, а сугубо ради преодоления ограничений реализации DI в Symfony. Фактически это попытка натянуть rich model на Symfony DI.

Так какие ограничения? Обсуждаемое тут, что нельзя в конструкторе сущности пробросить зависимости? Это ограничения Doctrine. Вернее даже пробросить можно вроде, но при восстановлении не сохранится ничего. Навскидку, вижу два-три метода как можно это обойти в Symfony, да и вообще в PHP. Но у меня большие сомнения, что нужно.

Очень рекомендую изучить статью «Domain-Driven Design: создание домена» с примерами, которые более наглядно покажут то, что хотел сказать автор статьи. В свое время, очень сильно помогла всем прийти к DDD без боли.
Sign up to leave a comment.

Articles