Любые идеи / отзывы приветствуются 🙂
Я столкнулся с проблемой в том, как обрабатывать бизнес-логику вокруг моих объектов Doctrine2 в большом приложении Symfony2 . (Извините за длину сообщения)
После чтения многих блогов, поваренной книги и других ресурсов, я обнаружил, что:
Хорошо, я полностью согласен с этим, но: где и как обрабатывать сложные правила бизнеса на моделях домена?
НАШИ ДОМЕННЫЕ МОДЕЛИ:
- Группа может использовать роли
- Роль может использоваться различными группами
- Пользователь может принадлежать многим группам со многими ролями ,
На уровне сохранения 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)
API не представляют домен: из этой службы можно вызывать $group->removeRole($role)
. 2 – Внедрение в Менеджерах домена
Инкапсулируйте эту бизнес-логику в конкретный «администратор сущностей домена», также позвоните в Model Providers:
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) – каждый абонент фактически выполняет сервис, автоматически запускаемый в фоновом режиме Менеджером сущностей, и в более простых сценариях он может работать, но возникают проблемы, как только действие (добавление / удаление роли) потребует большого контекста например. который пользователь выполнил действие, с какой страницы или любой другой тип комплексной проверки.
См. Здесь: Sf2: использование службы внутри объекта
Может быть, мой ответ здесь помогает. Он просто обращается к этому: как «развязать» модель против стойкости против уровней контроллера.
В вашем конкретном вопросе я бы сказал, что здесь есть «трюк» … что такое «группа»? Это «один»? или это когда оно относится к кому-то?
Первоначально ваши классы Model могли бы выглядеть так:
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.
Это «правило» может явно выйти за пределы класса «Пользователь», «Группа» и «Роль» и оставить в классе более высокого уровня, который содержит «правила», с помощью которых пользователи могут присоединиться или покинуть группы.
Я настоятельно рекомендую посмотреть другой ответ.
Надеюсь помочь!
Хави.
Как личное предпочтение, мне нравится начинать просто и расти, когда применяются более бизнес-правила. Поэтому я склоняюсь к лучшему для слушателей .
Ты только
Что-то, что потребует много макетов / заглушек, если у вас есть один класс обслуживания, такой как:
class SomeService { function someMethod($argA, $argB) { // some logic A. ... // some logic B. ... // feature you want to test. ... // some logic C. ... } }
Я сторонник бизнес-знаний . Доктрина проделала долгий путь, чтобы не загрязнять вашу модель инфраструктурой; он использует отражение, поэтому вы можете изменять аксессоры, как хотите. 2 «Доктрина», которые могут оставаться в ваших сущностных классах, – это аннотации (вы можете избежать благодаря отображению YML) и ArrayCollection
. Это библиотека вне Doctrine ORM ( Doctrine/Common
), поэтому никаких проблем нет.
Таким образом, придерживаясь основ DDD, сущности на самом деле являются местом для размещения вашей логики домена. Конечно, иногда этого недостаточно, тогда вы можете бесплатно добавлять сервисы домена , услуги без проблем с инфраструктурой.
Репозитории Doctrine являются более средними: я предпочитаю сохранять их как единственный способ запроса для сущностей, событий, если они не придерживаются первоначального шаблона репозитория, и я бы скорее удалил сгенерированные методы. Добавление службы диспетчера для инкапсуляции всех операций fetch / save данного класса было распространенной практикой Symfony несколько лет назад, мне это совсем не нравится.
По моему опыту, у вас может возникнуть гораздо больше проблем с компонентом формы Symfony, я не знаю, используете ли вы его. Они будут строго ограничить вашу способность настраивать конструктор, тогда вы можете использовать именованные конструкторы. Добавление PhpDoc @deprecated̀
тега wil дает вашим парам некоторую визуальную обратную связь, они не должны предъявлять иск первоначальному конструктору.
И последнее, но не менее важное: слишком много полагаться на события Doctrine в конечном итоге укусит вас. В них слишком много технических ограничений, и я нахожу их трудными для отслеживания. При необходимости я добавляю события домена, отправленные из контроллера / команды, в диспетчер событий Symfony.
Я бы рассмотрел использование служебного слоя отдельно от самих объектов. Классы объектов должны описывать структуры данных и, в конечном счете, некоторые другие простые вычисления. Сложные правила идут на услуги.
Пока вы пользуетесь услугами, вы можете создавать более развязанные системы, службы и так далее. Вы можете воспользоваться преимуществами инъекции зависимостей и использовать события (диспетчеры и слушатели) для обмена информацией между службами, которые слабо связаны между собой.
Я говорю это на основе собственного опыта. В начале я использовал всю логику внутри классов сущностей (особенно, когда я разработал приложения symfony 1.x / doctrine 1.x). До тех пор, пока приложения выросли, их было очень сложно поддерживать.