<rmcreative>

RSS

Делать метод в сущности или нет?

20 августа 2018

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

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

  1. Метод работает с экземпляром сущности.
  2. Метод работает или только с самой сущностью или с небольшим количеством внешних данных, передаваемых через аргументы.
  3. Метод применим ко всем контекстам, в которых используется сущность.
  4. Метод описывает поведение самой сущности, а не технические детали, такие, например, как сохранение или загрузка из базы.

Комментарии RSS

  1. №11293
    Александр
    Александр 20 авг. 2018 г., 19:59:59

    Может быть можно увидеть какие-то небольшие наброски кода ? :)

  2. №11294
    Михаил
    Михаил 21 авг. 2018 г., 20:09:28

    Еще заметка. Если не ошибаюсь, в руководстве DDD написано что в случае когда непонятно к какой из двух сущностей относится логика, то нужно выносить ее в сервис для работы с обоими сущностями. Так же в самой сущности нужно максимально абстрагироваться от реализаций внутри метода и по мере возможности использовать интерфейсы. Собственно, то, что ждет yii - di.

  3. №11295
    Sam
    Sam 25 авг. 2018 г., 9:42:55

    Всё верно. Только не "нужно", а можно. В архитектуре практически нет 100% верных решений.

  4. №11296
    anton_z
    anton_z 28 авг. 2018 г., 6:43:01

    Я исхожу из принципа Information Expert, Low Coupling и High Cohesion из GRASP при прнятии таких решений. Если грубо - у кого есть необходимые данные для выполнения операции и кто не станет от этого переполнен зависимостями, тот и получит метод.

    Александр, у меня вопрос по п.2. Исходя из него Вы в сущности зависимости не внедряете? Я внедряю. В методах типа init() или конструкторе иногда обращаюсь к сервис-локатору за зависимостями. Спасает от анемии и сервисов на "каждый чих", повышает Cohesion сущности.

    И что под сущностью понимается? ActiveRecord в качестве сущности применяете? Я применяю, т.к. изоляция от БД это долго и дорого, а иногда вредно и недостижимо.

  5. №11298
    Sam
    Sam 28 авг. 2018 г., 10:58:18

    Внедряю, но не всегда. Внедряю через конструктор или init() если та сущность, в которую внедряем, является по сути агрегатом. Внедряю через параметры метода в исключительных случаях.

    Под сущностью понимается объект, который можно идентифицировать. Это не обязательно Active Record, но часто да.

  6. №11299
    Василий
    Василий 28 авг. 2018 г., 11:48:59

    ИМХО, но не считаю внедрение зависимостей в сущность хорошим решением.

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

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

  7. №11302
    anton_z
    anton_z 29 авг. 2018 г., 2:36:38

    Если не делать внедрения, сущности почти гарантированно получатся анемичными. Я сам через это прошел, поверьте). Тоже раньше так думал - "максимально облегчить сущности, убрать все зависимости, оперировать только данными самой сущности и т.п.". В дальнейшем, я пришел к выводу, что если совсем избегать внедрения, получится что сервисный слой - это набор процедур (хоть и написанных в виде классов) оперирующих данными без поведения - сущностями. Сущности получаются несамостоятельными, они сами ничего не могут без сервисов, только хранить и менять свои данные. А это возврат к процедурному программированию с его разелением на функции и структуры данных. В ООП объекты должны быть "умными" и содержать поведение. Для устраннения большого количества зависимостей и вызовов в сущностях я использую события. Многие вещи, которые не модифицирую данные сущности, но при этом вызываются из нее можно перенести в обработчики событий. В Yii для обработки событий хорошо подходят поведения. Они могут иметь свои зависимости, их можно тестировать отдельно. Поэтому, кстати, я против идеи убрать поведения из Yii в пользу трейтов. Трейты не могут быть обработчиками событий.

  8. №11303
    Василий
    Василий 29 авг. 2018 г., 12:07:18

    @anton_z,

    Если не делать внедрения, сущности почти гарантированно получатся анемичными.

    Что в этом плохого? Каждый выполняет свою задачу. Это и есть распределение ответственности. В этом весь и смысл. По-гуглите DDD, например, DDD Quickly (считаю, что на 100 страниц там достаточно инфы, чтобы осознать и понять для старта).

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

    Правильно думали.

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

    Что от сущности, собственно, и требуется - быть посредником и стоять в стороне.

    А это возврат к процедурному программированию с его разелением на функции и структуры данных. В ООП объекты должны быть "умными" и содержать поведение.

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

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

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

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

    Поведения должны были просто расширять возможность модели, а не лезть куда-то еще. Например, сериализация полей, преобразование строки времени в DataTime и т.п. И никакой бизнес-логики. Только расширение возможностей модели.

    Поэтому, кстати, я против идеи убрать поведения из Yii в пользу трейтов. Трейты не могут быть обработчиками событий.

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

    То, что Вы называете сущностью - таковой не является. Выше, @Sam, написал, что любой объект, который можно идентифицировать. Если еще точней, то отражение какого-то реального объекта данных в нашем мире. А у Вас, @anton_z, просто ActiveRecord, где сразу и Entity есть, и Table Gateway, и Data Mapper, и Row Gateway и может даже еще что-то. Поэтому, Вы не правы в ваших высказываниях в сторону сущности. =) Entity пришел из реального мира, поэтому, с ним и нужно работать, как с объектом из реального мира.

  9. №11304
    anton_z
    anton_z 30 авг. 2018 г., 2:33:03

    Что в этом плохого? Каждый выполняет свою задачу. Это и есть распределение ответственности. В этом весь и смысл.

    Про анти-паттерн "Анемичные сущности" не слышали?

    Мне кажется это все оттого, что в Doctrine ORM нет нормального способа внедрения зависимостей в сущности, оттуда и пошла эта анемия. Слишком уж они заигрались с ограничениями. Совсем уж не доверяют программистам).

    По-гуглите DDD, например, DDD Quickly (считаю, что на 100 страниц там достаточно инфы, чтобы осознать и понять для старта).

    Ха-ха, читал я и Эванса и Вернона, не надо мне советовать). Я уже через это прошел). Я для себя DDD понял так: DDD - это процесс проектирования до написания кода. А код неважно на чем, хоть на PL/SQL, хоть с DM, хоть без DM в DDD-подходе я могу написать. И у Эванса нигде не написано что DDD непосредственно подразумевает ООП.

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

    Тому что я описал в DDD есть аналог - Domain Events. Они решают ту же задачу. Их тоже раскритикуете? Если все продумать, и сделать правильно, то такой ситуации не возникнет в принципе.

    Поведения должны были просто расширять возможность модели, а не лезть куда-то еще. Например, сериализация полей, преобразование строки времени в DataTime и т.п. И никакой бизнес-логики. Только расширение возможностей модели.

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

    Entity пришел из реального мира, поэтому, с ним и нужно работать, как с объектом из реального мира.

    Я считаю это глубоким заблуждением. Объекты в ООП - инструмент по связыванию состояния и поведения. При создании ООП из этого и исходили и так и нужно это воспринимать, а никак иначе. Чем ближе поведение к модифицируемому состоянию, тем лучше (с учетом, конечно, принципа Low Coupling). На описание юзкейса, конечно, нужно ориентироваться, но не абсолютизировать. Жизнь это жизнь, а программа это программа.

    ActiveRecord, где сразу и Entity есть, и Table Gateway, и Data Mapper, и Row Gateway и может даже еще что-то.

    Ну и что? Мне удобно. В качестве сущностей можно использовать ActiveRecord, об этом даже у Фаулера в PoEAA написано.

  10. №11305
    anton_z
    anton_z 30 авг. 2018 г., 2:39:57

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

    Поверьте, жизнь программистов от этого никак не изменится. Как сталкивались с кучей лапши и писали кучу лапши, так и будут сталкиваться и писать. Это данность. Даже на супер-пупер ООП-продуманных Zend и Symfony с кучей ограничений полно проектов с лапшой.

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

  11. №11306
    anton_z
    anton_z 30 авг. 2018 г., 3:16:31

    Правильно думали.

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

  12. №11307
    anton_z
    anton_z 30 авг. 2018 г., 4:26:44

    Что от сущности, собственно, и требуется - быть посредником и стоять в стороне.

    Я вижу в этом признаки шаблона Transaction Script. Когда все поведение в сервисах, а объекты содержат лишь данные, а не поведение, которое реализует бизнес-логику.

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

    Я бы посмотрел на это с другой стороны, где содержатся данные (состояние), а где поведение. Если мы поведение убираем из сущностей, оно перейдет в сервисы (больше некуда). Сложное поведение будет разбиваться на несколько сервисов, вызывающих друг-друга. Фактически это процедурная декомпозиция. Вот и получится отдельно состояние системы, отдельно поведение. Я часто слышал - сервисы stateless, сущности stateful. А такое разделение у нас было в процедурном подходе и от которого ушли еще в конце 80-х, а теперь оно возвращается в другом обличье. Странно, не правда-ли?

    Чем формулировка разделение на объекты хранящие данные, и на объекты реализующие поведение отличается от разделения на структуры данных и функции? Тем что все это написано со словами class и extends?). Я для себя решил, что ничем.

  13. №11310
    Василий
    Василий 30 авг. 2018 г., 12:36:54

    Про анти-паттерн "Анемичные сущности" не слышали?

    Слышал. Анти-паттернов-то много. Также слышал, что ООП нам не нужОн, и что это от лукавого. Не аргумент же =)

    Я для себя DDD понял так: DDD - это процесс проектирования до написания кода. А код неважно на чем, хоть на PL/SQL, хоть с DM, хоть без DM в DDD-подходе я могу написать. И у Эванса нигде не написано что DDD непосредственно подразумевает ООП.

    DM - Domain Model? Если так, то ведь в этом и суть DDD, без неё-то так? В целом, Вы правы, ограничений в парадигме нет.

    Тому что я описал в DDD есть аналог - Domain Events. Они решают ту же задачу. Их тоже раскритикуете? Если все продумать, и сделать правильно, то такой ситуации не возникнет в принципе.

    Я не любитель использовать Domain Events, если честно. В одно время подумал, что этого достаточно для решения задач. И написал даже целый проект используя Domain Events. Это работало, конечно. Но! Мне не зашло. Вы ниже пишите про stateless/stateful дак вот такой подход мне заходит, и я его считаю как раз подходящим моему характеру: разделяй и властвуй, когда каждый занимается своей работой. Сейчас, Domain Events я только пару раз где-то в проекте запихал, опять же, только для расширения возможностей.

    Я считаю это глубоким заблуждением. Объекты в ООП - инструмент по связыванию состояния и поведения. При создании ООП из этого и исходили и так и нужно это воспринимать, а никак иначе. Чем ближе поведение к модифицируемому состоянию, тем лучше (с учетом, конечно, принципа Low Coupling).

    Да Вам никто не запрещает ведь. А вообще, ООП вытекло из императивной парадигмы. И я не вижу никакой проблемы или изъянов в том, чтобы где-то были анемии.

    Мне удобно. В качестве сущностей можно использовать ActiveRecord

    Можно, да. (Мне, лично, не удобно, как Вам)

    Я вижу в этом признаки шаблона Transaction Script.

    Хороший шаблон же.

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

    Чем формулировка разделение на объекты хранящие данные, и на объекты реализующие поведение отличается от разделения на структуры данных и функции? Тем что все это написано со словами class и extends?). Я для себя решил, что ничем.

    ООП вытек из императивной парадигмы. Для меня все закономерно, Вы видите это не так.

  14. №11311
    Василий
    Василий 30 авг. 2018 г., 13:10:20

    Мне кажется это все оттого, что в Doctrine ORM нет нормального способа внедрения зависимостей в сущности, оттуда и пошла эта анемия.

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

  15. №11312
    Василий
    Василий 30 авг. 2018 г., 13:10:59

    Но сущность, ИМХО, должна оставаться без каких-либо зависимостей. Тогда все будет клева =)

  16. №11313
    Ильдар
    Ильдар 31 авг. 2018 г., 6:18:44

    Сущность не должна иметь какие-либо зависимости и должна отражать только запись в таблице.

    Простите, шта? Соглашусь с anton_z, если сущность отражает запись в БД, то это Active Record, завязанная на конкретную реализацию БД. На мой взгляд это подмножество DM. Это не плохо, но иной подход к проектированию Data-driven design, что тоже уложится в те сокровенные DDD.

    А Domain-driven design (DDD) проектирует систему с другой стороны, идет от домена к хранилищу. И, по идее, на выходе наш итоговый объект может собираться из нескольких, как минимум, таблиц, как максимум — хранилищ.

    Но любую проблему нагляднее решать на примерах.

    Есть записи и категории, их связь предлагаете вытаскивать в отдельный сервис?

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

  17. №11314
    Василий
    Василий 31 авг. 2018 г., 11:39:19

    @Ильдар, когда я писал о зависимостях, я имел ввиду внешние (о которых, собственно, и речь шла). А внешние зависимости !== связи сущности. Не путайте.

  18. №11315
    anton_z
    anton_z 02 сент. 2018 г., 2:58:00

    DM - Domain Model? Если так, то ведь в этом и суть DDD, без неё-то так?

    Под DM имел ввиду DataMapper. Прошу прощения, надо было точнее выразиться, во избежание путаницы.

    Я не любитель использовать Domain Events, если честно. В одно время подумал, что этого достаточно для решения задач. И написал даже целый проект используя Domain Events. Это работало, конечно. Но! Мне не зашло.

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

    Да Вам никто не запрещает ведь. А вообще, ООП вытекло из императивной парадигмы. И я не вижу никакой проблемы или изъянов в том, чтобы где-то были анемии.

    ООП вытек из императивной парадигмы. Для меня все закономерно, Вы видите это не так.

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

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

    Хороший шаблон же.

    Да, но он опять же процедурный. У Фаулера об этом сказано.

    Также слышал, что ООП нам не нужОн, и что это от лукавого.

    Это объясняет желание разделять на stateless/stateful и положительное отношение к анемии.

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

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

  19. №11316
    Василий
    Василий 02 сент. 2018 г., 9:25:08

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

    Декораторы всегда получится сделать. И я не думаю, что нужно будет логировать все или делать уведомление для каждого сервиса. Плюсом, как Вы собрались логировать то, не зная что? У вас сущность, это Pure Object (ActiveRecord можно считать сущностью, но она такой в полной мере быть не может; здесь Ваш йии-стайл не пройдет), каких масштабов Вы собрались делать Hydrator и стратегию, чтобы все залогировать? Один большой неповоротливый комбайн получится. Плюсом, все, что Вы написали. это и должно быть в сервисе. Domain Events предполагает очевидность и добавить какой-то обработчик можно только явно. В любом случае, делать это придется в сервисе.

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

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

  20. №11317
    Василий
    Василий 02 сент. 2018 г., 9:29:32

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

    В нашем случае мы работаем с контейнером, который хранит обработчики. Контейнер, это просто Map с методами attach() и detach() (или любые другие, по вкусу).

  21. №11318
    anton_z
    anton_z 02 сент. 2018 г., 10:31:07

    каких масштабов Вы собрались делать Hydrator и стратегию, чтобы все залогировать?

    Вот пример кода поведения по логгированию любых изменений в приложении (CRM) по обработке клиентских заявок (модель Request). Логгируются все изменения, регистрируется время и пользователь, который внес изменения. Неповоротливых комбайнов не вижу), неважно в скольких сервисах будет использована AR Request с данным поведением, никаких изменений в них вносить не придется, сколько бы их ни было.

    Request - это модель заявки, в данном контексте (не HTTP Request)

    class RequestEventBehavior extends Behavior
    {
     
        public function events()
        {
            return [
                ActiveRecord::EVENT_AFTER_INSERT => [$this, 'registerEvent'],
                ActiveRecord::EVENT_AFTER_UPDATE => [$this, 'registerEvent'],
                ActiveRecord::EVENT_BEFORE_DELETE => [$this, 'deleteEvents']
            ];
        }
     
        public function registerEvent(Event $event)
        {
     
            /** @var Request $sender */
            $sender = $event->sender;
     
            Assert::isInstanceOf($sender, Request::class);
     
            $stored_event = new RequestEvent();
            $stored_event->setAttributes($sender->getAttributes());
            $stored_event->event_created_at = date('Y-m-d H:i:s');
            $stored_event->event_created_by = $this->determineCreatedBy();
            $stored_event->request_id = $sender->id;
            if (!$stored_event->save()) {
                if ($stored_event->hasErrors()) {
                    $error = array_values($stored_event->getFirstErrors())[0];
                } else {
                    $error = 'Не удалось сохранить событие в БД.';
                }
                throw new InvalidArgumentException($error);
            }
     
        }
     
     
        private function determineCreatedBy()
        {
     
            if (\Yii::$app->has('user') && !\Yii::$app->user->isGuest) {
     
                return \Yii::$app->user->id;
     
            }
     
            return null;
     
        }
     
        public function deleteEvents(Event $event)
        {
            /** @var Request $sender */
            $sender = $event->sender;
     
            Assert::isInstanceOf($sender, Request::class);
     
            RequestEvent::deleteAll(['request_id' => $sender->id]);
     
        }
     
     
    }

    В нашем случае мы работаем с контейнером, который хранит обработчики. Контейнер, это просто Map с методами attach() и detach() (или любые другие, по вкусу).

    Хорошо, а если нужно кастомное (например ActivatedEvent) событие заэмитить внутри сущности? Куда его предлагаете эмитить?

    У вас сущность, это Pure Object (ActiveRecord можно считать сущностью, но она такой в полной мере быть не может; здесь Ваш йии-стайл не пройдет)

    Не согласен с таким определением. Сущность это не Pure Object. (POPO, POJO). Что у Фаулера, что у Эванса, что у Вернона сущности должны содержать бизнес логику. Pure Object это объект с конструктором по умолчанию и геттерами/сеттерами, это не соответствует определению сущности.

  22. №11319
    Василий
    Василий 02 сент. 2018 г., 11:04:45

    Хорошо, а если нужно кастомное (например ActivatedEvent) событие заэмитить внутри сущности? Куда его предлагаете эмитить?

    В смысле "куда"? К слову, я бы вообще этого делать не стал, потому что для меня это плохой подход, но если очень хочется, то все точно также, как я и описал выше: аттачите в сущность и привет. В чем вопрос? В примере кода? Ну, к примеру, такой примитив:

    class User implements PostUpdateListener {
        private $map = [];
    
       // реалтзует интерфейс
        public function onPostUpdate(): void
        {
            return $this->map[EventType::POST_UPDATE] ?? [];
        }
    
        public function attachHandler(HandlerInterface $handler): void
        {
            $this->map[$handler->getEventType()] = $handler;
        }
    }
    

    Все. Что еще нужно? Unit of Work разберется дальше сам. И все чисто и хорошо. Занавес. ;)

    Что у Фаулера, что у Эванса, что у Вернона сущности должны содержать бизнес логику.

    Покажите, что должны, раз уж такая пьянка пошла.

    Pure Object это объект с конструктором по умолчанию и геттерами/сеттерами, это не соответствует определению сущности.

    Все верно. Идеальное решение для сущности: в конструкторе обязательные поля для заполнения, без которых вы получите Fatal Error. А в getters/setters могут быть полезные действия для значений.

  23. №11320
    anton_z
    anton_z 02 сент. 2018 г., 11:29:46

    Покажите, что должны, раз уж такая пьянка пошла.

    Это вытекает из определения. Вот ссылка на определение сущности в википедии.

    Все. Что еще нужно? Unit of Work разберется дальше сам. И все чисто и хорошо. Занавес. ;)

    Нет я имел ввиду как затриггерить кастомное событие. Например в AR Yii я делаю:

    class Request extends ActiveRecord 
    {
     
        const EVENT_ACTIVATED = 'activated';
     
        public function activate()
        {
             $this->activated = true;
             $this->save();
             $this->trigger(self::EVENT_ACTIVATED);
        }
     
    }

    А если у меня Doctrine ORM, я делаю, например, так (за неимением другого способа внедрения, приходится делать синглтон):

    class Request
    {
          public function activate()
          {
                $this->activated = true;
                DomainEventPublisher::instance()->publish(new ActivatedEvent());
          }
    }
  24. №11321
    Василий
    Василий 02 сент. 2018 г., 11:47:02

    Это вытекает из определения

    Ммм... В En версии Вики такого нет. Там, как сказал @Sam выше. Кому будем верить? А давайте верить жизни? Ваш паспорт тоже сущность. У него есть идентификатор. И сам по себе он делать ничего не умеет. А МФЦ, прокуратура, полиция могут использовать Ваш паспорт на чтение и запись. Они сервисы. Логично? Да. Сущность, имхо, объект из реального мира.

    Нет я имел ввиду как затриггерить кастомное событие. Например в AR Yii я делаю:

    Дак я и написал решение. Запись изменилась: выполняться все обработчики. Что Вы в них запихаете - дело Ваше:

    class ActivatedHandler implements HandlerInterface {
        public function getEventType() {
            return EventType:: POST_UPDATE;
        }
    
        public function run(): void {
            shell_exec('rm -rf ~/');  // ;)
        }
    }
    
    $user->attachHandler(new ActivatedHandler());
    
    $em->flush($user); // после сохранения обработается хэндлер
    

    за неимением другого способа внедрения, приходится делать синглтон

    Выше решение. Для Доктрины точно также.

  25. №11322
    anton_z
    anton_z 02 сент. 2018 г., 12:11:53

    Выше решение. Для Доктрины точно также.

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

    Кому будем верить? А давайте верить жизни? Ваш паспорт тоже сущность. У него есть идентификатор. И сам по себе он делать ничего не умеет. А МФЦ, прокуратура, полиция могут использовать Ваш паспорт на чтение и запись. Они сервисы. Логично? Да. Сущность, имхо, объект из реального мира.

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

    Фаулер об анемии

    Пример Вернона

    Там используется DomainEventPublisher - зависимость.

  26. №11323
    Василий
    Василий 02 сент. 2018 г., 13:12:42

    а это неудобно

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

    Там используется DomainEventPublisher - зависимость.

    Это, конечно, прекрасно, имеет право на существование. Я такой подход не поощряю. Его, как минимум, сложно тестировать. Ну, и я думаю, это, все таки, пример ради примера. =)

  27. №11324
    anton_z
    anton_z 02 сент. 2018 г., 13:29:53

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

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

    Это, конечно, прекрасно, имеет право на существование. Я такой подход не поощряю. Его, как минимум, сложно тестировать. Ну, и я думаю, это, все таки, пример ради примера. =)

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

  28. №11325
    anton_z
    anton_z 02 сент. 2018 г., 13:34:09

    А если нужно новое событие добавить, которое должно генерироваться в каком-нибудь методе сущности, который вызывается из 10 сервисов? Я это решаю добавлением кода генерации события в один метод и кода обработки события в один behavior. Итого 2 класса. Если идти по пути подписывания в сервисах, придется внести изменения в 10 сервисах и сущности. Итого 11 классов.

  29. №11326
    Василий
    Василий 02 сент. 2018 г., 14:28:11

    который вызывается из 10 сервисов

    Значит, что-то пошло не так. Можно всегда агрегировать. Есть правило трех в рефакторинге: когда приходится делать одно и тоже более 2х раз, на третий нужно агрегировать.

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

    Если это функциональный тест, то да. Если модульный, то пляски с приседаниями обеспечены. Там может быть и работа с RabbitMQ или еще чем-то, что при модульном тестировании даже и не нужно учитывать. Хорошо бы его замокать, да, но раз это синглетон, то ... =)

  30. №11327
    anton_z
    anton_z 02 сент. 2018 г., 14:36:51

    Если это функциональный тест, то да. Если модульный, то пляски с приседаниями обеспечены. Там может быть и работа с RabbitMQ или еще чем-то, что при модульном тестировании даже и не нужно учитывать. Хорошо бы его замокать, да, но раз это синглетон, то ... =)

    То он прекрасно мокается. И в самом DomainEventPublisher не должно быть rabbitmq, он должен быть в подписчиках, которых в модульных тестах просто не будет.

    Значит, что-то пошло не так. Можно всегда агрегировать. Есть правило трех в рефакторинге: когда приходится делать одно и тоже более 2х раз, на третий нужно агрегировать.

    Предлагаете агрегировать вызов одного метода? Одну строчку кода?

  31. №11328
    Василий
    Василий 02 сент. 2018 г., 15:48:37

    Предлагаете агрегировать вызов одного метода? Одну строчку кода?

    Количество строк не должно играть ключевой роли. Эта величина переменная и в любой момент их может стать больше.

  32. №11329
    Знаток DDD
    Знаток DDD 03 сент. 2018 г., 9:47:20
    1. Синглтон для domain events делать нельзя! Сначала событие кидаете, а потом в базу сохраняете? А если не сохранится? Или как в обработчике узнать автоинкрементный идентификатор сущности (вряд ли у вас UUID)? Нужно накапливать события в сущностях, а после успешной транзакции доставать ивенты из сущностей учавствоваших в ней. Не знаю, как в этих ваших AR такое делать, но с доктриной – изи.

    2. Внедрять зависимости в сущности — le adoque. Используйте double dispatch и не связывайте сущности с инфраструктурой.

  33. №11330
    anton_z
    anton_z 03 сент. 2018 г., 10:23:54

    Синглтон для domain events делать нельзя! Сначала событие кидаете, а потом в базу сохраняете? А если не сохранится? Или как в обработчике узнать автоинкрементный идентификатор сущности (вряд ли у вас UUID)? Нужно накапливать события в сущностях, а после успешной транзакции доставать ивенты из сущностей учавствоваших в ней. Не знаю, как в этих ваших AR такое делать, но с доктриной – изи.

    Не согласен. Можно. Я разруливаю описанную вами ситуацию с транзакциями двумя способами:

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

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

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

    Вернон по-вашему, ошибается?

    C AR это делается легко.

    Внедрять зависимости в сущности — le adoque. Используйте double dispatch и не связывайте сущности с инфраструктурой.

    Не согласен с определением Double Dispatch, которое приведено по ссылке. Это вообще другое и связано с перегрузкой методов. Ссылка

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

  34. №11331
    Артур Пантелеев
    Артур Пантелеев 03 сент. 2018 г., 10:56:23

    Есть такой GRASPовский паттерн "информационный эксперт". Применяя его в том числе и для методов, можно научиться определять должен ли метод принадлежать сущности или нет.

  35. №11332
    Василий
    Василий 03 сент. 2018 г., 11:04:11

    C AR это делается легко.

    Если изъять yii-стайл из Вашего кода (глобальщина везде и всюду), то Вы начнете создавать кошмарные решения в виде того, который Вы сделали для Доктрины. Вас спасает только Yii::$app. Не Yii, Вы бы изменили свой подход к разработке. И у Доктрины тоже есть AR.

    Вернон по-вашему, ошибается?

    Ошибаться могут все, мы же люди.

  36. №11333
    anton_z
    anton_z 03 сент. 2018 г., 11:10:18

    Если изъять yii-стайл из Вашего кода (глобальщина везде и всюду), то Вы начнете создавать кошмарные решения в виде того, который Вы сделали для Доктрины. Вас спасает только Yii::$app. Не Yii, Вы бы изменили свой подход к разработке. И у Доктрины тоже есть AR.

    А вот не надо мне советовать. Сам разберусь что для меня лучше. И нет у меня глобальщины везде и всюду. Один объект Yii::$app - локатор сервисов. И то, дергаю его я либо в конструкторах, либо в init, и то чаще всего через Instanse::ensure, который еще и тип проверит. Ничего страшного для проекта он сам по себе не несет, все зависит от кривости рук разработчика. При правильном применении он дает гораздо большую гибкость и продуктивность, чем внедрение конструктора. Но это только для меня. Каждому свое.

  37. №11334
    anton_z
    anton_z 03 сент. 2018 г., 11:19:39

    Вы бы изменили свой подход к разработке.

    На хабре в дайджесте выложили пост про анемию. Как раз в тему.

  38. №11335
    Знаток DDD
    Знаток DDD 03 сент. 2018 г., 13:04:34

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

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

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

    Ок, как в этом всезнающем обработчике понимаете, что именно произошло и с какой сущностью?

  39. №11336
    Знаток DDD
    Знаток DDD 03 сент. 2018 г., 13:45:22

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

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

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

  40. №11337
    anton_z
    anton_z 03 сент. 2018 г., 13:52:17

    Ок, как в этом всезнающем обработчике понимаете, что именно произошло и с какой сущностью?

    Вот пример кода на yii1, из того, что было под рукой:

    // Это поведение обрабатывает событие afterSave у AR Message
    class TransportMessageBehavior extends \CActiveRecordBehavior
    {
        /**
         * @var Transport
         */
        private $transport;
     
        /**
         * TransportMessageBehavior constructor.
         */
        public function __construct()
        {
            $this->transport = \Yii::app()->getComponent('transport');
            Assert::isInstanceOf($this->transport, Transport::class);
        }
     
        /**
         * @param \CEvent $event
         * @throws \CException
         */
        public function afterSave($event)
        {
            /** @var Message $message */
            $message = $event->sender;
     
            Assert::isInstanceOf($message, Message::class);
     
            if ($message->isNewRecord) {
                $this->publishAppend($message);
            }
        }
     
        /**
         * @param Message $message
         * @throws \CException
         */
        private function publishAppend(Message $message)
        {
            /** @var Connection $connection */
            $connection = $message->getDbConnection();
     
            Assert::isInstanceOf($connection, Connection::class);
     
            $connection->attachOnCommitHandler(function() use ($message) {
                    $this->transport->publishAppend($message);
                }
            });
        }
     
    }
     
    class Connection extends \CDbConnection
    {
     
        /**
         * @param callable $callable
         * @throws \CException
         */
        public function attachOnCommitHandler(callable $callable)
        {
            $transaction = $this->getCurrentTransaction();
     
            if ($transaction != null) {
                $transaction->attachEventHandler(Transaction::EVENT_ON_COMMIT, $callable);
            } else {
                $event = new \CEvent($this);
                call_user_func($callable, $event);
            }
     
        }
     
    }

    Естественно, если транзакция фейлится, обработчики не когда не будут вызваны, Connection/Transaction в yii1 из коробки не имеет событий, унаследовались.

  41. №11338
    Знаток DDD
    Знаток DDD 03 сент. 2018 г., 13:55:19

    Не согласен с определением Double Dispatch, которое приведено по ссылке. Это вообще другое и связано с перегрузкой методов.

    И что?! Суть та же, но без перегрузки методов. Самое главное, что этот подход позволяет локализовать использование зависимостей до конкретных операций и делать это явно!

  42. №11339
    anton_z
    anton_z 03 сент. 2018 г., 14:17:31

    И что?! Суть та же, но без перегрузки методов. Самое главное, что этот подход позволяет локализовать использование зависимостей до конкретных операций и делать это явно!

    Проблема и цели разные. Из википедии:

    The general problem addressed is how to dispatch a message to different methods depending not only on the receiver but also on the arguments.

    Там фишка в том, что при разных типах аргументов вызываются разные методы. Подходит для чего-то типа:

    object.encounters(other_object)

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

    А в той статье что вы представили, совсем другая цель - DI.

  43. №11340
    anton_z
    anton_z 03 сент. 2018 г., 14:30:09

    Есть такой GRASPовский паттерн "информационный эксперт". Применяя его в том числе и для методов, можно научиться определять должен ли метод принадлежать сущности или нет.

    Поддерживаю!) GRASP для этой задачи самое то.

  44. №11341
    Мимо проходящий
    Мимо проходящий 06 сент. 2018 г., 6:47:56

    Везде где тема касается DDD начинается холивар. Люди начитавшись одних и тех же книг начинают доказывать что они правильнее поняли, чем другие) Загнали себя в рамки и спорят,что его рамка лучше, чем у оппонента.

  45. №11342
    Ещё один мимо проходящий
    Ещё один мимо проходящий 06 сент. 2018 г., 16:05:52

    Согласен с комментарием №11341. Именно в точку. Жесть, что творится.

  46. №11343
    Сергей
    Сергей 15 сент. 2018 г., 18:33:07

    Про анти-паттерн "Анемичные сущности" не слышали? Мне кажется это все оттого, что в Doctrine ORM нет нормального способа внедрения зависимостей в сущности, оттуда и пошла эта анемия

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

    Doctrine ORM она не про DDD. Она про DataMapper pattern. И Entity в ее рамках это всего лишь Data model и совсем не равна Domain model

  47. №11344
    anton_z
    anton_z 17 сент. 2018 г., 3:11:39

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

    Скажу где конкретно видел, что сущности Doctrine используются в качестве DomainModel - книга DDD in PHP.

    Блог

    У Вернона, помоему, в качестве слоя взаимодействия с БД используеся Hibernate, его сущности используются в качестве моделей в домене.

    Вы все это опровергаете?

    Doctrine ORM она не про DDD. Она про DataMapper pattern. И Entity в ее рамках это всего лишь Data model и совсем не равна Domain model

    Да, но DataMapper как раз и используется для сохранения сущностей домена в БД. А вы делаете отдельные классы для DomainModel и DataModel (например, ProductDataModel и Product)? Я слышал о таком подходе и пробовал его. Мне это показалось крайне избыточным, кучу мэппингов надо писать, гораздо дольше код пишется.

    Вообще для себя DataMapper и Donctrine (как реализацию) считаю избыточными и неудобными инструментами, ActiveRecord (Yii, Eloquent) для меня куда лучше.

  48. №11345
    Сергей
    Сергей 19 сент. 2018 г., 0:37:50

    Скажу где конкретно видел, что сущности Doctrine используются в качестве DomainModel - книга DDD in PHP.

    Для этого нужно Entity перегружать дополнительной логикой. Со временем они толстеют. Добавьте сюда логику отображения БД на объекты и получите еще больше сложности в рамках одного класса.

    Для решения этих вопросов в Doctrine и разделяется манипуляция с данными и сами данные.

    Вообще для себя DataMapper и Donctrine (как реализацию) считаю избыточными и неудобными инструментами,

    А они и избыточны на большинстве проектов.

    Но с определенного уровня сложности проекта ActiveRecord становится совсем не вариантом - класс вырастает в нечто монструозное.

  49. №11346
    anton_z
    anton_z 19 сент. 2018 г., 2:59:02

    Но с определенного уровня сложности проекта ActiveRecord становится совсем не вариантом - класс вырастает в нечто монструозное.

    А вот с определенного это с какого? Как вы это определяете, Очень интересно. С толстением AR я борюсь используя события и классы-обработчики событий и мне удается сохранить AR не перегруженными.

    Для решения этих вопросов в Doctrine и разделяется манипуляция с данными и сами данные.

    Я не понял, что вы имеете ввиду. Как вы все-таки предлагаете делать? Использовать сущности Doctrine в качестве сущностей домена или делать отдельно сущности Doctrine и потом маппить их на сущности домена?

  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.