Encje doktrynalne i logika biznesowa w aplikacji Symfony

Wszelkie pomysły / opinie są mile widziane :)

Napotkałem problem w tym, jak obsługiwać logikę biznesową wokół moich doctrine2 entities w dużej } aplikacji Symfony2. (Sorry za Długość posta)

Po przeczytaniu wielu blogów, książek kucharskich i innych ressources, stwierdzam, że:

    W 2011 roku w ramach programu"Horyzont 2020"w ramach programu" Horyzont 2020 "zrealizowano program ramowy" Horyzont 2020 " - program ramowy w zakresie badań naukowych i innowacji (2014-2020)]}
  • Kontrolery muszą być tym bardziej szczupłe,
  • modele domen należy go oddzielić od warstwy persistence (entity do not know entity manager)
Ok, całkowicie się z tym Zgadzam, ale : gdzie i jak radzić sobie ze złożonymi regułami biznesowymi dotyczącymi modeli domen ?

Prosty przykład

NASZE MODELE DOMEN:

  • Grupa może używać ról
  • a rola może być używana przez różne grupy
  • a użytkownik może należeć do wielu grup z wieloma rolami ,

W warstwieSQL persistence możemy modelować te relacje jako:

Tutaj wpisz opis obrazka

NASZE SZCZEGÓŁOWE ZASADY BIZNESOWE:

  • użytkownik może mieć role w grupach tylko wtedy, gdy role są dołączone do grupy .
  • jeśli odłączymy rolę R1 od Grupy G1 , to wszystkie role R1 muszą być deleted

Jest to bardzo prosty przykład, ale chciałbym poznać najlepszy sposób (y) zarządzania tymi regułami biznesowymi.


Znaleziono rozwiązania

1- implementacja w warstwie usług

Użyj określonej klasy usług jako:

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();   
}
  • (+) jedna usługa na klasę / na regułę biznesową
  • (-) encje API nie reprezentują domeny : możliwe jest wywołanie $group->removeRole($role) z tej usługi.
  • (-) też wiele klas usług w dużej aplikacji ?

2 - implementacja w domenie entity Managers

[4]} Encapsulate these business Logic in specific "domain entities manager", also call 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) {...}
}
  • (+) wszystkie reguły są scentralizowane
  • (-) encje API nie reprezentują domeny : możliwe jest wywołanie $group->removeRole($role) z usługi...
  • (-) menedżerowie domen stają się menedżerami FAT ?

3 - Użyj słuchaczy, gdy to możliwe

Użyj symfony i/lub doctrine event listeners:

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 - implementacja bogatych modeli poprzez rozszerzenie encji

Użyj encji jako podrzędnej / nadrzędnej Klasy klas modeli domen, które zawierają wiele logiki domen. Ale to rozwiązanie wydaje mi się bardziej zdezorientowane.


Dla Ciebie, jaki jest najlepszy sposób(y) zarządzać tę logikę biznesową, koncentrując się na bardziej czyste, oddzielone, testowalny kod ? Wasze opinie i Dobre praktyki ? Masz konkretne przykłady ?

Główne Źródła:

Author: Community, 2013-10-03

5 answers

Uważam rozwiązanie 1) za najłatwiejsze do utrzymania z dłuższej perspektywy. Rozwiązanie 2 prowadzi nadętą klasę "Menedżera", która ostatecznie zostanie podzielona na mniejsze kawałki.

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

"zbyt wiele klas usług w dużej aplikacji" nie jest powodem do unikania SRP.

Jeśli chodzi o język domeny, uważam następujący kod podobny:

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

I

$group->removeRole($role);

Również z tego, co Ty opisane, usunięcie / dodanie roli z grupy wymaga wielu zależności (zasada inwersji zależności) i może to być trudne z menedżerem FAT/nadęty.

Rozwiązanie 3) wygląda bardzo podobnie do 1) - każdy abonent jest w rzeczywistości usługą automatycznie wyzwalaną w tle przez Entity Managera i w prostszych scenariuszach może działać, ale problemy pojawią się, gdy tylko akcja (dodanie / usunięcie roli)będzie wymagała dużo kontekstu np. który użytkownik wykonał akcję, z jakiej strony lub innej rodzaj walidacji złożonej.

 3
Author: Tomas Dermisek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-08 03:52:00

Zobacz tutaj: Sf2: korzystanie z usługi wewnątrz podmiotu

Może moja odpowiedź pomoże. Dotyczy to tylko tego: jak "odsprzęgnąć" model vs persistance vs warstwy kontrolera.

W twoim konkretnym pytaniu, powiedziałbym, że jest tu "sztuczka"... co to jest "grupa"? "Sam"? czy to, gdy odnosi się do kogoś?

Początkowo Twoje klasy modelowe prawdopodobnie wyglądałyby tak:

UserManager (service, entry point for all others)

Users
User
Groups
Group
Roles
Role

UserManager miałby metody pobierania obiektów modelu (jak powiedziano w tej odpowiedzi, nigdy nie powinieneś robić new). W kontrolerze można to zrobić:

$userManager = $this->get( 'myproject.user.manager' );
$user = $userManager->getUserById( 33 );
$user->whatever();

Wtedy... User, jak mówisz, może mieć role, które mogą być przypisane lub nie.

// 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 );
}

Uprościłem, oczywiście można dodać po Id, dodać po obiekcie itp.

Ale kiedy myślisz o tym w "języku naturalnym"... zobaczmy...

    Wiem, że Alice należy do fotografów.
  1. dostaję obiekt Alice.
  2. Zapytuję Alice o grupy. I get the grupowych fotografów.
  3. pytam fotografów o role.

Zobacz więcej szczegółów:

  1. wiem, że Alice jest użytkownikiem id = 33 i jest w grupie fotografa.
  2. proszę Alice do Usermanagera przez $user = $manager->getUserById( 33 );
  3. Nie jest to jednak możliwe, ponieważ nie jest to możliwe, ponieważ nie jest to możliwe.]}
  4. chciałbym zobaczyć role grupy... Co mam zrobić?
    • Opcja 1: $group- > getRoles ();
    • Opcja 2: $ group- > getRolesForUser ($userId);

Drugi jest jakby zbędny, ponieważ dostałem grupę przez Alice. Możesz utworzyć nową klasę GroupSpecificToUser, która dziedziczy z Group.

Podobne do gry... co to jest gra? "Gra" jako "szachy" w ogóle? Czy ta konkretna " gra " w "szachy", którą zaczęliśmy wczoraj?

W tym przypadku $user->getGroups() zwróci zbiór grup obiektów.

GroupSpecificToUser extends Group
{
    User getPointOfViewUser()
    Roles getRoles()
}

To drugie podejście pozwoli Ci zamknąć tam wiele innych rzeczy, które wcześniej czy później się pojawią: czy ten użytkownik może coś tutaj zrobić? możesz po prostu odpytywać podklasę grupy: $group->allowedToPost();, $group->allowedToChangeName();, $group->allowedToUploadImage();, itd.

W każdym razie, można uniknąć tworzenia taht weird class i po prostu zapytać użytkownika o te informacje, jak podejście $user->getRolesForGroup( $groupId );.

Model nie jest warstwą persistance

Lubię "zapomnieć" o perystancji, gdy projektowanie. Zazwyczaj siedzę z moim zespołem (lub ze sobą, dla projektów osobistych) i spędzam 4 lub 6 godzin na rozmyślaniu przed napisaniem dowolnej linii kodu. Piszemy API w dokumencie txt. Następnie iterować na nim dodawanie, usuwanie metod, itp.

Możliwe API" punktu wyjścia " dla Twojego przykładu może zawierać zapytania o cokolwiek, jak trójkąt:
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.

Wydarzenia

Jak wspomniano w wskazanym artykule, rzuciłbym również zdarzenia w modelu,

Na przykład, gdy usuwając rolę od użytkownika w grupie, mogłem wykryć w "słuchaczu", że jeśli był to ostatni administrator, mogę a) anulować usunięcie roli, b) zezwolić na to i opuścić grupę bez administratora, c) zezwolić na to, ale wybrać nowego administratora z użytkowników w grupie, itp lub jakąkolwiek politykę jest odpowiednia dla Ciebie.

W ten sam sposób, być może użytkownik może należeć tylko do 50 grup (jak w LinkedIn). Możesz wtedy po prostu rzucić Zdarzenie preAddUserToGroup i każdy łapacz może zawierać Regulamin zabraniający tego, gdy użytkownik chce dołączyć do grupy 51.

Ta "reguła" może wyraźnie opuścić klasę User, Group i Role i opuścić klasę wyższego poziomu, która zawiera "reguły", według których użytkownicy mogą dołączać lub opuszczać grupy.

Zdecydowanie sugeruję, aby zobaczyć drugą odpowiedź.

Mam nadzieję pomóc!

Xavi.

 5
Author: Xavi Montero,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:00:24

Jako osobiste preferencje Lubię zaczynać prosto i rozwijać się, gdy stosuje się więcej reguł biznesowych. Jako taki mam tendencję do faworyzowania słuchacze podchodzą lepiej .

You just

  • dodaj więcej słuchaczy w miarę rozwoju reguł biznesowych ,
  • każdy ma jedną odpowiedzialność ,
  • i możeszprzetestować tych słuchaczy niezależnie łatwiej.

Coś, co wymagałoby wielu kpin / stubów, jeśli masz jedną usługę klasa:

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

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
 2
Author: jorrel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-08 06:52:12

Jestem zabiznesowymi podmiotami. Doctrine idzie długą drogę, aby nie zanieczyszczać Twojego modelu problemami z infrastrukturą ; używa refleksji, więc możesz dowolnie modyfikować Accesory, jak chcesz. 2" doktrynalne " rzeczy, które mogą pozostać w klasach encji, to adnotacje (których można uniknąć dzięki mapowaniu YML)oraz ArrayCollection. Jest to biblioteka poza doktryną ORM (Doctrine/Common), więc nie ma tam żadnych problemów.

Więc, trzymając się podstaw DDD, byty są naprawdę miejscem, w którym można umieścić twoja logika domeny. Oczywiście czasami to nie wystarczy, wtedy możesz dodać domain services , usługi bez obaw o infrastrukturę.

Doctrine repozytoria są bardziej pośrednie: wolę je zachować jako jedyny sposób na zapytanie o encje, event, jeśli nie trzymają się pierwotnego wzorca repozytorium i wolałbym usunąć wygenerowane metody. Dodanie usługi manager do enkapsulacji wszystkich operacji fetch/save danej klasy było powszechna praktyka Symfonii kilka lat temu, nie bardzo mi się to podoba.

Z mojego doświadczenia wynika, że możesz mieć znacznie więcej problemów z komponentem Symfony form, Nie wiem czy go używasz. Będą one serisouly ograniczyć możliwość dostosowania konstruktora, wtedy można raczej używać nazwanych konstruktorów. Dodanie znacznika PhpDoc @deprecated̀ spowoduje, że Twoje pary otrzymają wizualną informację zwrotną, której nie powinny pozwać oryginalnego konstruktora.

Wreszcie, ale nie mniej ważne, poleganie zbytnio na wydarzeniach doktrynalnych w końcu ugryzie ty. Są tam zbyt wiele ograniczeń technicznych, Plus uważam, że trudno je śledzić. W razie potrzeby dodaję zdarzenia domeny wysłane z kontrolera/polecenia do dyspozytora zdarzeń Symfony.

 0
Author: romaricdrigon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-24 14:13:33

Rozważyłbym użycie warstwy usługowej oprócz samych Bytów. Klasy encji powinny opisywać struktury danych i ewentualnie inne proste obliczenia. Złożone zasady idą do usług.

Dopóki korzystasz z usług, możesz tworzyć więcej systemów, usług itd. Możesz skorzystać z dependency injection i wykorzystać zdarzenia (dyspozytorów i słuchaczy) do komunikacji między usługami, utrzymując je słabo połączone.

I say that na podstawie własnego doświadczenia. Na początku umieszczałem całą logikę wewnątrz klas encji (szczególnie gdy tworzyłem symfony 1.x / Doktryna 1.x aplikacji). Tak długo, jak aplikacje rosły, stały się naprawdę trudne do utrzymania.

 0
Author: Omar Alves,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-10 17:18:00