Сущности доктрины и бизнес-логика в приложении 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 RoleUserManager будет иметь методы получения объектов модели (как сказано в этом ответе, вы никогда не должны делать
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.х приложений). Пока приложения росли, их было очень трудно поддерживать.