Сущности доктрины и бизнес-логика в приложении Symfony
Любые идеи/отзывы приветствуются :)
Я столкнулся с проблемой в том, как обрабатывать бизнес-логику вокруг моих сущностей Доктрины2 в большом приложении Symfony2. (Извините за длину поста)
Прочитав множество блогов, кулинарных книг и других ресурсов, я обнаружил, что:
- Сущности могут использоваться только для сохранения отображения данных ("анемичная модель"),
- Контроллеры должны быть как можно более тонкими,
- Домен модели должны быть отделены от уровня сохраняемости (сущность не знает менеджера сущностей)
Хорошо, я полностью согласен с этим, но : где и как обрабатывать сложные правила ведения бизнеса в моделях предметной области ?
Простой пример
НАШИ ДОМЕННЫЕ МОДЕЛИ :
- группа может использовать Роли
- роль может использоваться различными группами
- пользователь может принадлежать многим Группы со многими Ролями,
В SQL уровне сохраняемости мы могли бы смоделировать эти отношения следующим образом:
НАШИ ОСОБЫЕ БИЗНЕС-ПРАВИЛА :
- Пользователь может иметь Роли в Группах только если Роли присоединены к группе.
- Если мы отделим Роль R1 от Группы G1, все Роли пользователя, связанные с Группой 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 и/или Doctrine:
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/Доктрина, нужно ли вводить бизнес-логику в мой контроллер? И дублирующий контроллер?
- Расширение сущности Доктрины с целью добавления бизнеса логика
- 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 answers
Я нахожу решение 1) как самое простое для поддержания в долгосрочной перспективе. Решение 2 приводит к раздутому классу "Менеджер", который в конечном итоге будет разбит на более мелкие части.
Http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData - имя объекта управления
"Слишком много классов обслуживания в большом приложении" - это не причина избегать SRP.
С точки зрения языка домена я нахожу следующий код похожим:
$groupRoleService->removeRoleFromGroup($role, $group);
И
$group->removeRole($role);
Также из чего вы описали, что для удаления/добавления роли из группы требуется множество зависимостей (принцип инверсии зависимостей), и это может быть сложно с ТОЛСТЫМ/раздутым менеджером.
Решение 3) очень похоже на 1) - каждый подписчик на самом деле является услугой, автоматически запускаемой в фоновом режиме менеджером сущностей, и в более простых сценариях это может работать, но проблемы возникнут, как только действие (добавление/удаление роли) потребует много контекста, например. какой пользователь выполнил действие, с какой страницы или любого другой тип комплексной проверки.
Смотрите здесь: 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=33, и она в группе Фотографа.
- Я запрашиваю Алису у менеджера пользователей через
$user = $manager->getUserById( 33 );
- Я получаю доступ к фотографам группы через Алису, возможно, с помощью `$group = $user->getgroupbyname('Фотографы');
- Затем я хотел бы увидеть роли группы... Что мне делать?
- Вариант 1: $группа->getRoles();
- Вариант 2: $group->getrolesforuser($идентификатор пользователя);
Второе похоже на избыточное, так как я получил группу через Алису. Вы можете создать новый класс GroupSpecificToUser
, который наследует от Group
.
Похоже на игру... что такое игра? "Игра" как "шахматы" в целом? Или конкретная "игра" в "шахматы", которую мы с тобой начали вчера?
В этом случае $user->getGroups()
вернет коллекцию GroupSpecificToUser объекты.
GroupSpecificToUser extends Group
{
User getPointOfViewUser()
Roles getRoles()
}
Этот второй подход позволит вам инкапсулировать множество других вещей, которые рано или поздно появятся: Разрешено ли этому пользователю что-то здесь делать? вы можете просто запросить подкласс группы: $group->allowedToPost();
, $group->allowedToChangeName();
, $group->allowedToUploadImage();
, и т.д.
В любом случае вы можете избежать создания странного класса и просто спросить пользователя об этой информации, например, о подходе $user->getRolesForGroup( $groupId );
.
Модель не является слоем устойчивости
Мне нравится "забывать" о постоянстве, когда проектирование. Обычно я сижу со своей командой (или с самим собой, для личных проектов) и провожу 4 или 6 часов, просто размышляя, прежде чем написать какую-либо строку кода. Мы пишем API в текстовом документе. Затем повторите его, добавляя, удаляя методы и т.д.
Возможный 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.
События
Как сказано в указанной статье, я бы также добавил события в модель,
Например, когда удаляя роль у пользователя в группе, я мог бы обнаружить в "прослушивателе", что, если это был последний администратор, я могу а) отменить удаление роли, б) разрешить ее и оставить группу без администратора, в) разрешить ее, но выбрать нового администратора из числа пользователей в группе и т. Д. Или любую другую политику, подходящую для вас.
Точно так же, возможно, пользователь может принадлежать только к 50 группам (как в LinkedIn). Затем вы можете просто запустить событие preAddUserToGroup, и любой ловец может содержать набор правил, запрещающих это, когда пользователь хочет присоединиться к группе 51.
Это "правило" может явно выходить за пределы класса пользователей, групп и ролей и переходить в класс более высокого уровня, содержащий "правила", по которым пользователи могут вступать в группы или покидать их.
Я настоятельно рекомендую посмотреть другой ответ.
Надеюсь помочь!
Хави.
В качестве личного предпочтения мне нравится начинать с простого и расти по мере применения новых бизнес-правил. Поэтому я склоняюсь к тому, чтобы слушатели подходили лучше.
Ты просто
- добавляйте больше слушателей по мере развития бизнес-правил,
- каждый из них несет единственную ответственность,
- и вы можете протестировать этих слушателей независимо проще.
Что-то, что потребовало бы большого количества насмешек/заглушек, если у вас есть один класс обслуживания, такой как:
class SomeService
{
function someMethod($argA, $argB)
{
// some logic A.
...
// some logic B.
...
// feature you want to test.
...
// some logic C.
...
}
}
Я за бизнес-ориентированные организации. Доктрина имеет большое значение для того, чтобы не загрязнять вашу модель проблемами инфраструктуры; она использует отражение, поэтому вы можете изменять методы доступа по своему усмотрению.
2 "Доктрина", которые могут остаться в ваших классах сущностей, - это аннотации (их можно избежать благодаря сопоставлению YML) и ArrayCollection
. Это библиотека за пределами доктрины ORM (Doctrine/Common
), так что никаких проблем там нет.
Итак, придерживаясь основ DDD, сущности - это действительно то место, где можно поместите свою доменную логику. Конечно, иногда этого недостаточно, тогда вы можете добавить доменные сервисы , сервисы без проблем с инфраструктурой.
Доктрина репозитории являются более промежуточными: я предпочитаю использовать их в качестве единственного способа запроса сущностей, если они не соответствуют исходному шаблону репозитория, и я бы предпочел удалить сгенерированные методы. Добавление службы диспетчера для инкапсуляции всех операций извлечения/сохранения данного класса было обычная практика Symfony несколько лет назад, мне это не совсем нравится.
По моему опыту, у вас может возникнуть гораздо больше проблем с компонентом формы Symfony, я не знаю, используете ли вы его. Они серьезно ограничат вашу способность настраивать конструктор, тогда вы можете использовать именованные конструкторы. Добавление тега PHPDoc @deprecated̀
даст вашим парам некоторую визуальную обратную связь, они не должны подавать в суд на оригинальный конструктор.
И последнее, но не менее важное: слишком большая опора на события Доктрины в конечном итоге укусит ты. Там слишком много технических ограничений, плюс я нахожу, что их трудно отслеживать. При необходимости я добавляю события домена , отправляемые контроллером/командой в диспетчер событий Symfony.
Я бы рассмотрел возможность использования сервисного уровня отдельно от самих сущностей. Классы сущностей должны описывать структуры данных и, в конечном счете, некоторые другие простые вычисления. Сложные правила относятся к услугам.
Пока вы используете сервисы, вы можете создавать более разобщенные системы, сервисы и так далее. Вы можете воспользоваться преимуществами внедрения зависимостей и использовать события (диспетчеры и прослушиватели) для обеспечения связи между службами, сохраняя их слабую связь.
Я говорю это на основе моего собственного опыта. Вначале я использовал всю логику внутри классов сущностей (особенно когда я разрабатывал приложения symfony 1.x/doctrine 1.x). По мере того как приложения росли, их становилось очень трудно поддерживать.