Сущности доктрины и бизнес-логика в приложении 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-реализация богатых моделей путем расширения сущностей

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


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

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

5 53

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
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, добавить по объекту и т. д.

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

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

Смотрите подробнее:

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