Сущности доктрины и бизнес-логика в приложении Symfony


Любые идеи/отзывы приветствуются :)

Я столкнулся с проблемой в том, как обрабатывать бизнес-логику вокруг моих сущностей Доктрины2 в большом приложении Symfony2. (Извините за длину поста)

Прочитав множество блогов, кулинарных книг и других ресурсов, я обнаружил, что:

  • Сущности могут использоваться только для сохранения отображения данных ("анемичная модель"),
  • Контроллеры должны быть как можно более тонкими,
  • Домен модели должны быть отделены от уровня сохраняемости (сущность не знает менеджера сущностей)

Хорошо, я полностью согласен с этим, но : где и как обрабатывать сложные правила ведения бизнеса в моделях предметной области ?


Простой пример

НАШИ ДОМЕННЫЕ МОДЕЛИ :

  • группа может использовать Роли
  • роль может использоваться различными группами
  • пользователь может принадлежать многим Группы со многими Ролями,

В SQL уровне сохраняемости мы могли бы смоделировать эти отношения следующим образом:

enter image description here

НАШИ ОСОБЫЕ БИЗНЕС-ПРАВИЛА :

  • Пользователь может иметь Роли в Группах только если Роли присоединены к группе.
  • Если мы отделим Роль 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 - Внедряйте расширенные модели путем расширения сущностей

Используйте сущности в качестве подкласса/родительского класса классов моделей предметной области, которые инкапсулируют множество логики предметной области. Но это решение кажется мне более запутанным.


Для вас, каков наилучший способ(ы) управления этой бизнес-логикой, сосредоточив внимание на более чистой, отделенной, тестируемый код? Ваши отзывы и рекомендации? У вас есть конкретные примеры?

Основные Ресурсы :

Author: Community, 2013-10-03

5 answers

Я нахожу решение 1) как самое простое для поддержания в долгосрочной перспективе. Решение 2 приводит к раздутому классу "Менеджер", который в конечном итоге будет разбит на более мелкие части.

Http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData - имя объекта управления

"Слишком много классов обслуживания в большом приложении" - это не причина избегать SRP.

С точки зрения языка домена я нахожу следующий код похожим:

$groupRoleService->removeRoleFromGroup($role, $group);

И

$group->removeRole($role);

Также из чего вы описали, что для удаления/добавления роли из группы требуется множество зависимостей (принцип инверсии зависимостей), и это может быть сложно с ТОЛСТЫМ/раздутым менеджером.

Решение 3) очень похоже на 1) - каждый подписчик на самом деле является услугой, автоматически запускаемой в фоновом режиме менеджером сущностей, и в более простых сценариях это может работать, но проблемы возникнут, как только действие (добавление/удаление роли) потребует много контекста, например. какой пользователь выполнил действие, с какой страницы или любого другой тип комплексной проверки.

 3
Author: Tomas Dermisek, 2013-10-08 03:52:00

Смотрите здесь: 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 );
}

Я упростил, конечно, вы могли бы добавлять по идентификатору, добавлять по объекту и т.д.

Но когда вы думаете об этом на "естественном языке"... давайте посмотрим...

  1. Я знаю, что Элис принадлежит к фотографам.
  2. Я получаю объект Алисы.
  3. Я спрашиваю Алису о группах. Я получаю фотографы группы.
  4. Я спрашиваю фотографов о ролях.

Подробнее см.:

  1. Я знаю, что Алиса - пользователь id=33, и она в группе Фотографа.
  2. Я запрашиваю Алису у менеджера пользователей через $user = $manager->getUserById( 33 );
  3. Я получаю доступ к фотографам группы через Алису, возможно, с помощью `$group = $user->getgroupbyname('Фотографы');
  4. Затем я хотел бы увидеть роли группы... Что мне делать?
    • Вариант 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.

Это "правило" может явно выходить за пределы класса пользователей, групп и ролей и переходить в класс более высокого уровня, содержащий "правила", по которым пользователи могут вступать в группы или покидать их.

Я настоятельно рекомендую посмотреть другой ответ.

Надеюсь помочь!

Хави.

 5
Author: Xavi Montero, 2017-05-23 12:00:24

В качестве личного предпочтения мне нравится начинать с простого и расти по мере применения новых бизнес-правил. Поэтому я склоняюсь к тому, чтобы слушатели подходили лучше.

Ты просто

  • добавляйте больше слушателей по мере развития бизнес-правил,
  • каждый из них несет единственную ответственность,
  • и вы можете протестировать этих слушателей независимо проще.

Что-то, что потребовало бы большого количества насмешек/заглушек, если у вас есть один класс обслуживания, такой как:

class SomeService 
{
    function someMethod($argA, $argB)
    {
        // some logic A.
        ... 
        // some logic B.
        ...

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
 2
Author: jorrel, 2013-10-08 06:52:12

Я за бизнес-ориентированные организации. Доктрина имеет большое значение для того, чтобы не загрязнять вашу модель проблемами инфраструктуры; она использует отражение, поэтому вы можете изменять методы доступа по своему усмотрению. 2 "Доктрина", которые могут остаться в ваших классах сущностей, - это аннотации (их можно избежать благодаря сопоставлению YML) и ArrayCollection. Это библиотека за пределами доктрины ORM (Doctrine/Common), так что никаких проблем там нет.

Итак, придерживаясь основ DDD, сущности - это действительно то место, где можно поместите свою доменную логику. Конечно, иногда этого недостаточно, тогда вы можете добавить доменные сервисы , сервисы без проблем с инфраструктурой.

Доктрина репозитории являются более промежуточными: я предпочитаю использовать их в качестве единственного способа запроса сущностей, если они не соответствуют исходному шаблону репозитория, и я бы предпочел удалить сгенерированные методы. Добавление службы диспетчера для инкапсуляции всех операций извлечения/сохранения данного класса было обычная практика Symfony несколько лет назад, мне это не совсем нравится.

По моему опыту, у вас может возникнуть гораздо больше проблем с компонентом формы Symfony, я не знаю, используете ли вы его. Они серьезно ограничат вашу способность настраивать конструктор, тогда вы можете использовать именованные конструкторы. Добавление тега PHPDoc @deprecated̀ даст вашим парам некоторую визуальную обратную связь, они не должны подавать в суд на оригинальный конструктор.

И последнее, но не менее важное: слишком большая опора на события Доктрины в конечном итоге укусит ты. Там слишком много технических ограничений, плюс я нахожу, что их трудно отслеживать. При необходимости я добавляю события домена , отправляемые контроллером/командой в диспетчер событий Symfony.

 0
Author: romaricdrigon, 2016-06-24 14:13:33

Я бы рассмотрел возможность использования сервисного уровня отдельно от самих сущностей. Классы сущностей должны описывать структуры данных и, в конечном счете, некоторые другие простые вычисления. Сложные правила относятся к услугам.

Пока вы используете сервисы, вы можете создавать более разобщенные системы, сервисы и так далее. Вы можете воспользоваться преимуществами внедрения зависимостей и использовать события (диспетчеры и прослушиватели) для обеспечения связи между службами, сохраняя их слабую связь.

Я говорю это на основе моего собственного опыта. Вначале я использовал всю логику внутри классов сущностей (особенно когда я разрабатывал приложения symfony 1.x/doctrine 1.x). По мере того как приложения росли, их становилось очень трудно поддерживать.

 0
Author: Omar Alves, 2017-11-10 17:18:00