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

50

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

У меня возникла проблема с обработкой бизнес-логики вокруг моих объектов Doctrine2 в большом приложении Symfony2. (Извините за длину сообщения)

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

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

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


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

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

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

На уровне сохранения SQL мы могли бы моделировать эти отношения как:

Изображение 630

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

  • Пользователь может иметь Роли в Группы, только если к группе добавлены роли.
  • Если мы отделим Роль 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) из службы...
  • (-) Менеджеры домена становятся менеджерами FAT?

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, которые инкапсулируют много логики домена. Но для меня это кажется более смущенным.


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

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

Теги:
domain-driven-design
doctrine2

5 ответов

3
Лучший ответ

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

http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData

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

В терминах домена, я нахожу следующий код похожим:

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

и

$group->removeRole($role);

Кроме того, из того, что вы описали, удаление/добавление роли из группы требует много зависимостей (принцип инверсии зависимостей), и это может быть затруднено с помощью FAT/раздутого менеджера.

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

  • 0
    Спасибо за ваш отзыв. В подходе DDD я нахожу $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 все еще немного смущает меня.
5

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

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

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

Подробнее:

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

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

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

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

Хави.

  • 2
    Действительно хорошие ответы. Мне нравится, как вы отделяете модель от сущности, но у меня есть некоторые проблемы с пониманием того, как связать User (model) с User (persistable entity) с наблюдаемым шаблоном. userManager->getUserById(Id id) вернет User (model) загруженного из User (entity) используя, например, UserRepository (doctrine repository) . Это правильно? Итак, есть 2 метода «getUserById», один в диспетчере (возвращающий модель), другой в хранилище (возвращающий сущность)? Как менеджер связывает их?
  • 1
    Да, менеджер возвращает модель, а репозиторий возвращает сущность. Контроллеры и представления никогда не видят хранилище. Один из способов «представить перед кодированием» заключается в следующем: даже если нет необходимости делать следующее, представьте, что кто-то требует, чтобы ваше приложение могло работать либо из базы данных при работе на обычном сервере, либо из хранилища файлов в формате json когда приложение запускается в супер-крошечной встроенной системе, которая работает на PHP, но не запускает mysql. Представьте, что в ваших settings.yml есть что-то вроде хранилища: доктрина или хранилище: json (продолжение в следующем сообщении)
Показать ещё 1 комментарий
2

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

Вы просто

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

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

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

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
0

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

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

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

0

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню