Любые идеи/отзывы приветствуются:)
У меня возникла проблема с обработкой бизнес-логики вокруг моих объектов Doctrine2 в большом приложении Symfony2. (Извините за длину сообщения)
После чтения многих блогов, поваренной книги и других ресурсов, я обнаружил, что:
Хорошо, я полностью согласен с этим, но: где и как обрабатывать сложные бизнес-правила на моделях домена?
НАШИ ДОМЕННЫЕ МОДЕЛИ:
- a Группа может использовать Роли
- a Роль может использоваться различными группами
- Пользователь может принадлежать многим группам со многими Ролями,
На уровне сохранения SQL мы могли бы моделировать эти отношения как:
НАШИ КОНКРЕТНЫЕ ПРАВИЛА БИЗНЕСА:
- Пользователь может иметь Роли в Группы, только если к группе добавлены роли.
- Если мы отделим Роль 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();
}
$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) {...}
}
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 - Внедрение Rich Models путем расширения объектов
Использовать Entities как класс sub/parent классов Domain Models, которые инкапсулируют много логики домена. Но для меня это кажется более смущенным.
Для вас наилучшим способом управлять этой бизнес-логикой, фокусируясь на более чистом, развязаемом, проверяемом коде? Ваши отзывы и передовые методы? У вас есть конкретные примеры?
Основные ресурсы:
Я нахожу решение 1) как самое легкое, чтобы поддерживать его с большей точки зрения. Решение 2 приводит к раздутому классу "Менеджер", который в конечном итоге будет разбит на более мелкие куски.
http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData
"Слишком много классов обслуживания в большом приложении" не является основанием для исключения SRP.
В терминах домена, я нахожу следующий код похожим:
$groupRoleService->removeRoleFromGroup($role, $group);
и
$group->removeRole($role);
Кроме того, из того, что вы описали, удаление/добавление роли из группы требует много зависимостей (принцип инверсии зависимостей), и это может быть затруднено с помощью FAT/раздутого менеджера.
Решение 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, добавить Object и т.д.
Но когда вы думаете об этом в "естественном языке"... давайте посмотрим...
Подробнее:
$user = $manager->getUserById( 33 );
Второй подобен избыточному, поскольку я получил группу через Алису. Вы можете создать новый класс GroupSpecificToUser
, который наследует от Group
.
Как в игре... что такое игра? "Игра" как "шахматы" вообще? Или конкретная "игра" "шахмат", с которой мы с вами начали вчера?
В этом случае $user->getGroups()
вернет коллекцию объектов GroupSpecificToUser.
GroupSpecificToUser extends Group
{
User getPointOfViewUser()
Roles getRoles()
}
Этот второй подход позволит вам инкапсулировать там много других вещей, которые появятся рано или поздно: разрешено ли этому пользователю что-то делать здесь? вы можете просто запросить групповой подкласс: $group->allowedToPost();
, $group->allowedToChangeName();
, $group->allowedToUploadImage();
и т.д.
В любом случае вы можете избежать создания taht weird class и просто спросить пользователя об этой информации, например, подход $user->getRolesForGroup( $groupId );
.
Модель не является уровнем сопротивления
Мне нравится "забывать" о прогрессе при проектировании. Обычно я сижу со своей командой (или с самим собой, для личных проектов) и трачу 4 или 6 часов, просто задумываясь, прежде чем писать какую-либо строку кода. Мы пишем API в dxt dxt. Затем повторите попытку добавления, удаления методов и т.д.
Возможная "начальная точка" 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.
События
Как сказано в указанной статье, я бы также выбросил события в модели,
Например, при удалении роли от пользователя в группе я мог бы обнаружить в "слушателе", что если это был последний администратор, я могу: а) отменить удаление роли, b) разрешить ее и оставить группа без администратора, c) разрешить ее, но выбрать нового администратора с помощью пользователей в группе и т.д. или любой другой подходящий для вас вариант.
Точно так же, возможно, пользователь может принадлежать только 50 группам (как в LinkedIn). Затем вы можете просто выбросить событие preAddUserToGroup, и любой зрелище может содержать набор правил, запрещающих это, когда пользователь хочет присоединиться к группе 51.
Это "правило" может явно выйти за пределы класса "Пользователь", "Группа" и "Роль" и оставить в классе более высокого уровня, который содержит "правила", с помощью которых пользователи могут присоединиться или покинуть группы.
Я настоятельно рекомендую посмотреть другой ответ.
Надеюсь помочь!
Хави.
User (model)
с User (persistable entity)
с наблюдаемым шаблоном. userManager->getUserById(Id id)
вернет User (model)
загруженного из User (entity)
используя, например, UserRepository (doctrine repository)
. Это правильно? Итак, есть 2 метода «getUserById», один в диспетчере (возвращающий модель), другой в хранилище (возвращающий сущность)? Как менеджер связывает их?
Как личное предпочтение, мне нравится начинать просто и расти, когда применяются более бизнес-правила. Как правило, я предпочитаю слушателей лучше подходить.
Вы просто
Что-то, что потребует большого количества mocks/stubs, если у вас есть один класс обслуживания, например:
class SomeService
{
function someMethod($argA, $argB)
{
// some logic A.
...
// some logic B.
...
// feature you want to test.
...
// some logic C.
...
}
}
Я бы рассмотрел использование служебного слоя отдельно от самих сущностей. Классы объектов должны описывать структуры данных и, в конечном счете, некоторые другие простые вычисления. Сложные правила идут на сервисы.
Пока вы пользуетесь услугами, вы можете создавать более развязанные системы, службы и т.д. Вы можете воспользоваться преимуществами инъекции зависимостей и использовать события (диспетчеры и слушатели) для обмена данными между службами, поддерживающими их слабо связанными.
Я говорю это на основе собственного опыта. В начале я использовал всю логику внутри классов сущностей (особенно, когда я разработал приложения symfony 1.x/doctrine 1.x). До тех пор, пока приложения выросли, их было очень сложно поддерживать.
Я сторонник бизнес-ориентированных объектов. Доктрина проделала долгий путь, чтобы не загрязнять вашу модель инфраструктурой; он использует отражение, поэтому вы можете изменять аксессоры, как хотите.
2 "Доктрина", которые могут оставаться в ваших классах объектов, - это аннотации (вы можете избежать благодаря отображению YML) и ArrayCollection
. Это библиотека вне Doctrine ORM (Doctrine/Common
), поэтому никаких проблем нет.
Итак, придерживаясь основ DDD, сущности действительно являются местом для размещения вашей логики домена. Конечно, иногда этого недостаточно, тогда вы можете добавлять услуги домена, услуги без проблем с инфраструктурой.
Doctrine репозитории являются более средними: я предпочитаю, чтобы они были единственным способом запроса для сущностей, события, если они не придерживаются шаблона исходного репозитория, и я бы скорее удалил сгенерированный методы. Добавление службы менеджер для инкапсуляции всех операций fetch/save данного класса было распространенной практикой Symfony несколько лет назад, мне это совсем не нравится.
По моему опыту, у вас может возникнуть гораздо больше проблем с компонентом формы Symfony, я не знаю, используете ли вы его. Они будут строго ограничить вашу способность настраивать конструктор, тогда вы можете использовать именованные конструкторы. Добавление тега PhpDoc @deprecated̀
даст вашим парам некоторую визуальную обратную связь, они не должны предъявлять иск первоначальному конструктору.
И последнее, но не менее важное: слишком много полагаться на события в Доктрине в конечном итоге укусит вас. В них слишком много технических ограничений, и я нахожу их трудными для отслеживания. При необходимости я добавляю события домена, отправленные с контроллера/команды на диспетчер событий Symfony.
$group->removeRole($role)
более явным, но кажется, что его труднее реализовать с помощью сущностей Doctrine. Службы и слушатели часто используются в коде, который я прочитал. Я также часто сталкивался с классами менеджера , например, в FOS Bundles: github.com/FriendsOfSymfony/FOSCommentBundle/blob/master/Model/… или в Vespolina github.com/vespolina/commerce/blob/master/lib/Vespolina/Product/… но их ответственность VS Repository все еще немного смущает меня.