Сущности доктрины и бизнес-логика в приложении Symfony
любые идеи / обратная связь приветствуются :)
я столкнулся с проблемой в том, как обрабатывать бизнес-логику вокруг сущностей использует Doctrine2 большой Symfony2 приложение. (Извините за длину сообщения)
прочитав много блогов, поваренную книгу и другие ресурсы, я нахожу, что:
- сущности могут использоваться только для сохранения отображения данных ("анемичная модель"),
- контроллеры должны будь как можно стройнее,
- доменные модели должны быть отделены от уровня сохраняемости (entity do not know entity manager)
хорошо, я полностью согласен с этим, но : где и как обрабатывать сложные правила ведения бизнеса на моделях доменов ?
простой пример
НАШИ ДОМЕННЫЕ МОДЕЛИ:
- a группа можно использовать роли
- a роль может использоваться различными группы
- a пользователей может принадлежать многим группы много роли,
на SQL слой персистентности, мы могли бы смоделировать эти отношения как:
НАШИ КОНКРЕТНЫЕ БИЗНЕС-ПРАВИЛА:
- пользователей может быть роли in группытолько если роли присоединены к группе.
- если мы отсоединим a Роль R1 С Группа G1, все UserRoleAffectation с группой G1 и ролью R1 должны быть удалены
Это очень простой пример, но я хотел бы знаете лучший способ(ы) для управления этими бизнес-правил.
решения
1- Реализация в сервисном слое
использовать определенный класс обслуживания, как :
class GroupRoleAffectionService {
function linkRoleToGroup ($role, $group)
{
//...
}
function unlinkRoleToGroup ($role, $group)
{
//business logic to find all invalid UserRoleAffectation with these role and group
...
// BL to remove all found UserRoleAffectation OR to throw exception.
...
// detach role
$group->removeRole($role)
//save all handled entities;
$em->flush();
}
- (+) одна услуга на класс / на бизнес-правило
- (-) сущности API не представляют домен : можно вызвать
$group->removeRole($role)
выход из этой службы. - (-) слишком много классов обслуживания в большом приложении ?
2-Реализация в менеджерах доменных сущностей
инкапсулировать эти бизнес-логики в конкретном "менеджере доменных сущностей", также называют поставщиков моделей:
class GroupManager {
function create($name){...}
function remove($group) {...}
function store($group){...}
// ...
function linkRole($group, $role) {...}
function unlinkRoleToGroup ($group, $role)
{
// ... (as in previous service code)
}
function otherBusinessRule($params) {...}
}
- (+) все правила ведения бизнеса централизованы
- (-) сущности API не представляют домен : можно вызвать $group->removeRole($role) из службы...
- (-) менеджеры доменов становятся жирными менеджерами ?
3-Используйте слушателей, когда это возможно
используйте symfony и / или доктрину слушатели событий:
class CheckUserRoleAffectationEventSubscriber implements EventSubscriber
{
// listen when a M2M relation between Group and Role is removed
public function getSubscribedEvents()
{
return array(
'preRemove'
);
}
public function preRemove(LifecycleEventArgs $event)
{
// BL here ...
}
4-реализация богатых моделей путем расширения сущностей
использовать сущности в качестве суб / родительского класса классов моделей домена, которые инкапсулируют много логики домена. Но это решение кажется мне более запутанным.
для вас, каков наилучший способ (ы) управлять этой бизнес-логикой, сосредотачиваясь на более чистом, развязанном, тестируемом коде ? ваши отзывы и передовой опыт ? У вас есть бетон примеры ?
Основные Ресурсы :
- управляющие компании Symfony
- Symfony2 / Doctrine, чтобы поставить бизнес-логику в моем контроллере? А дублирующий контроллер?
- расширяя доктрину сущности для того, чтобы добавить бизнес логика
- http://iamproblematic.com/2012/03/12/putting-your-symfony2-controllers-on-a-diet-part-2/
- http://l3l0.eu/lang/en/2012/04/anemic-domain-model-problem-in-symfony2/
- https://leanpub.com/a-year-with-symfony
5 ответов:
Я нахожу решение 1) как самый простой для поддержания в долгосрочной перспективе. Решение 2 приводит к раздутому классу "менеджер", который в конечном итоге будет разбит на более мелкие куски.
http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData
"слишком много классов обслуживания в большом приложении" - это не причина избегать SRP.
С точки зрения языка домена, я нахожу следующий код похожим:
$groupRoleService->removeRoleFromGroup($role, $group);
и
$group->removeRole($role);
также из того, что вы описали, удаление/добавление роли из группы требует много зависимостей (принцип инверсии зависимостей), и это может быть трудно с жирным/раздутым менеджером.
решение 3) выглядит очень похоже на 1) - каждый подписчик на самом деле сервис автоматически запускается в фоновом режиме Entity Manager и в более простых сценариях он может работать, но проблемы возникнут, как только действие (добавление/удаление роли) потребует много контекста, например. какой пользователь выполнил действие,с какой страницы или любого другого типа комплексной проверки.
смотрите здесь: Sf2: использование службы внутри объекта
может быть, мой ответ здесь помогает. Он просто обращается к этому: как "развязать" модель против персистентности против уровней контроллера.
в вашем конкретном вопросе я бы сказал, что здесь есть" трюк"... что такое "группа"? Он "один"? или это когда он к кому-то относится?
первоначально ваши классы моделей, вероятно, могли бы выглядеть так:
UserManager (service, entry point for all others) Users User Groups Group Roles Role
UserManager будет иметь методы получения объектов модели (как сказано в этом ответе, вы никогда не должны делать
new
). В контроллере, вы можете сделать это:$userManager = $this->get( 'myproject.user.manager' ); $user = $userManager->getUserById( 33 ); $user->whatever();
затем...
User
, как вы говорите, могут быть роли, которые могут быть назначены или нет.// Using metalanguage similar to C++ to show return datatypes. User { // Role managing Roles getAllRolesTheUserHasInAnyGroup(); void addRoleById( Id $roleId, Id $groupId ); void removeRoleById( Id $roleId ); // Group managing Groups getGroups(); void addGroupById( Id $groupId ); void removeGroupById( Id $groupId ); }
я упростил, конечно, вы можете добавить по Id, добавить по объекту и т. д.
но когда вы думаете об этом на"естественном языке"... давай посмотрим...
- я знаю, что Алиса принадлежит к Фотографы.
- я получаю Алису объект.
- я спрашиваю Алису о группах. Я достаю групповых фотографов.
- я спрашиваю фотографов о ролях.
Смотрите подробнее:
- я знаю, что Алиса-пользователь id=33, и она находится в группе фотографа.
- я запрашиваю Алису в UserManager через
$user = $manager->getUserById( 33 );
- я получаю доступ к фотографам группы через Алису, возможно, с ' $group = $user - >getGroupByName ('Photographers');
- затем я хотел бы увидеть роли группы... Что же мне делать?
- Вариант 1: $group - >getRoles ();
- Вариант 2: $group- > getRolesForUser( $userId );
второй, как избыточный, как я получил группу через Алису. Вы можете создать новый класс
GroupSpecificToUser
, который наследует отGroup
.похоже на игру... что такое игра? "Игра" как "шахматы" внутри генерал? Или специфическая "игра" в "шахматы", которую мы с тобой начали вчера?
в этом случае
$user->getGroups()
возвращает коллекцию объектов GroupSpecificToUser.GroupSpecificToUser extends Group { User getPointOfViewUser() Roles getRoles() }
этот второй подход позволит вам инкапсулировать там много других вещей, которые появятся рано или поздно: этот пользователь может сделать что-то здесь? вы можете просто запросить подкласс группы:
$group->allowedToPost();
,$group->allowedToChangeName();
,$group->allowedToUploadImage();
и т. д.в любом случае, вы можете избежать создания taht странный класс и просто спросите пользователя об этой информации, например
$user->getRolesForGroup( $groupId );
подход.модель не является слоем персистентности
мне нравится "забывать" о перистанции при проектировании. Я обычно сижу со своей командой (или с самим собой, для личных проектов) и провожу 4 или 6 часов, просто думая, прежде чем писать какую-либо строку кода. Мы пишем API в txt doc. Затем повторите на нем добавление, удаление методов и т. д.
возможная" отправная точка " API для вашего примера может содержать запросы чего угодно, например треугольника:
User getId() getName() getAllGroups() // Returns all the groups to which the user belongs. getAllRoles() // Returns the list of roles the user has in any possible group. getRolesOfACertainGroup( $group ) // Returns the list of groups for which the user has that specific role. getGroupsOfRole( $role ) // Returns all the roles the user has in a specific group. addRoleToGroup( $group, $role ) removeRoleFromGroup( $group, $role ) removeFromGroup() // Probably you want to remove the user from a group without having to loop over all the roles. // removeRole() ?? // Maybe you want (or not) remove all admin privileges to this user, no care of what groups. Group getId() getName() getAllUsers() getAllRoles() getAllUsersWithRole( $role ) getAllRolesOfUser( $user ) addUserWithRole( $user, $role ) removeUserWithRole( $user, $role ) removeUser( $user ) // Probably you want to be able to remove a user completely instead of doing it role by role. // removeRole( $role ) ?? // Probably you don't want to be able to remove all the roles at a time (say, remove all admins, and leave the group without any admin) Roles getId() getName() getAllUsers() // All users that have this role in one or another group. getAllGroups() // All groups for which any user has this role. getAllUsersForGroup( $group ) // All users that have this role in the given group. getAllGroupsForUser( $user ) // All groups for which the given user is granted that role // Querying redundantly is natural, but maybe "adding this user to this group" // from the role object is a bit weird, and we already have the add group // to the user and its redundant add user to group. // Adding it to here maybe is too much.
событий
как сказано в указанной статье, я бы также бросил события в модель,
например, при удалении роли от пользователя в группе я мог бы обнаружить в "слушателе" , что если это был последний администратор, я могу a) отменить удаление роли, b) разрешить ее и оставить группу без администратора, c) разрешить ее, но выбрать нового администратора от пользователей в группе и т. д. или любой другой политики, подходящей для вас.
таким же образом, возможно, пользователь может принадлежать только к 50 группам (как в LinkedIn). Затем вы можете просто бросить событие preAddUserToGroup, и любой ловец может содержать набор правил запрета, когда пользователь хочет присоединиться к группе 51.
это "правило" может явно оставить вне класса пользователя, группы и роли и оставить в классе более высокого уровня, который содержит "правила", по которым пользователи могут присоединиться или оставьте группы.
я настоятельно рекомендую посмотреть другой ответ.
надеюсь на помощь!
Хави.
в качестве личного предпочтения, я хотел бы начать просто и расти, как больше бизнес-правил применяются. Как таковой я склонен благоволить слушатели подходят лучше.
просто
- добавить больше слушателей, как бизнес-правила развиваются,
- С единая ответственность,
- и проверьте эти слушатели самостоятельно легче.
что потребуется много насмешек/заглушек, если у вас есть один класс обслуживания, такой как:
class SomeService { function someMethod($argA, $argB) { // some logic A. ... // some logic B. ... // feature you want to test. ... // some logic C. ... } }
Я в пользу business-aware объекты. Доктрина проходит долгий путь, чтобы не загрязнять вашу модель проблемами инфраструктуры ; она использует отражение, поэтому вы можете изменять методы доступа по своему усмотрению. 2" доктрина " вещи, которые могут остаться в ваших классах сущностей являются аннотации (вы можете избежать благодаря YML mapping), и
ArrayCollection
. Это библиотека вне доктрины ОРМ (Doctrine/Common
), Так что никаких проблем.Итак, придерживаясь основ DDD, сущности являются действительно место, чтобы положить логику вашего домена. Конечно, иногда этого недостаточно, тогда вы можете добавить доменных служб, услуги без проблем с инфраструктурой.
доктрина репозитории являются более промежуточными: я предпочитаю хранить их как единственный способ запроса сущностей, событий, если они не придерживаются исходного шаблона репозитория, и я бы предпочел удалить сгенерированные методы. Добавление менеджер услуги для инкапсуляции всех операции выборки / сохранения данного класса были обычной практикой Symfony несколько лет назад, мне это не совсем нравится.
по моему опыту, вы можете столкнуться с гораздо большим количеством проблем с компонентом Symfony form, я не знаю, используете ли вы его. Они серьезно ограничат вашу способность настраивать конструктор,тогда вы можете использовать именованные конструкторы. Добавить PhpDoc
@deprecated̀
тег даст вашим парам некоторую визуальную обратную связь, они не должны подавать в суд на оригинальный конструктор.последнее, но не менее, слишком много полагаться на события доктрины в конечном итоге укусит вас. Там слишком много технических ограничений, плюс я нахожу их трудными для отслеживания. При необходимости я добавляю события домена отправлено из контроллера / команды в диспетчер событий Symfony.
Я бы рассмотрел использование уровня сервиса отдельно от самих объектов. Классы сущностей должны описывать структуры данных и в конечном итоге некоторые другие простые вычисления. Сложные правила идут в сервисы.
пока вы используете сервисы, вы можете создавать более разобщенные системы, сервисы и так далее. Вы можете воспользоваться преимуществами внедрения зависимостей и использовать события (диспетчеры и прослушиватели) для связи между службами, сохраняя их слабо соединенный.
Я говорю это на основании собственного опыта. В начале я использовал, чтобы поместить всю логику внутри классов сущностей (особенно когда я разработал symfony 1.х/учение 1.х приложений). Пока приложения росли, их было очень трудно поддерживать.