web-dev-qa-db-de.com

Doctrine Entities und Geschäftslogik in einer Symfony-Anwendung

Anregungen/Feedback sind willkommen :)

Ich habe ein Problem beim Umgang mit Geschäftslogik um meine Doctrine2-Entitäten in einer großen Symfony2-Anwendung . (Sorry für die Postlänge)

Nachdem ich viele Blogs, Kochbücher und andere Ressourcen gelesen habe, stelle ich fest, dass:

  • Entitäten dürfen nur für die Persistenz der Datenzuordnung verwendet werden ("anämisches Modell"),
  • Controller müssen möglichst schlank sein,
  • Domänenmodelle müssen von der Persistenzschicht entkoppelt sein (Entität kennt Entitätsmanager nicht)

Ok, da bin ich vollkommen einverstanden, aber: wo und wie werden komplexe Geschäftsregeln für Domain-Modelle gehandhabt?


Ein einfaches Beispiel

UNSERE DOMAIN-MODELLE:

  • a Gruppe kann Rollen verwenden
  • eine Rolle kann von verschiedenen Gruppen verwendet werden
  • ein Benutzer kann zu vielen Gruppen mit vielen Rollen gehören,

In einerSQLPersistenzschicht könnten wir diese Beziehungen wie folgt modellieren:

enter image description here

UNSERE SPEZIFISCHEN GESCHÄFTSREGELN:

  • Benutzer kann Rollen in Gruppen nur wenn Rollen an die Gruppe angehängt sind haben.
  • Wenn wir eine Rolle R1 von einer Gruppe G1 trennen, müssen alle UserRoleAffectation mit der Gruppe G1 und Rolle R1 gelöscht werden

Dies ist ein sehr einfaches Beispiel, aber ich möchte die besten Methoden zum Verwalten dieser Geschäftsregeln kennen.


Lösungen gefunden

1- Implementierung in Service Layer

Verwenden Sie eine bestimmte Serviceklasse als:

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();   
}
  • (+) ein Service pro Klasse/pro Geschäftsregel
  • (-) API-Entitäten stellen keine Domain dar: Es ist möglich, $group->removeRole($role) von diesem Service aus aufzurufen.
  • (-) Zu viele Serviceklassen in einer großen Anwendung?

2 - Implementierung in Domain Entity Managers

Verkapseln Sie diese Geschäftslogik in einem bestimmten "Domain Entity Manager". Rufen Sie auch die Modellanbieter auf:

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) {...}
}
  • (+) Alle Geschäftsregeln sind zentralisiert
  • (-) API-Entitäten stellen keine Domain dar: Es ist möglich, $ group-> removeRole ($ role) aus dem Dienst aufzurufen ...
  • (-) Domain Manager werden zu FAT Managern?

3 - Benutze Listener wenn möglich

Verwenden Sie Symfony- und/oder Doctrine-Ereignis-Listener:

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 - Implementieren Sie Rich Models, indem Sie Entities erweitern

Verwenden Sie Entities als Sub-/Parent-Klasse von Domain Models-Klassen, die einen Großteil der Domain-Logik einschließen. Diese Lösung erscheint mir jedoch verwirrender.


Was ist für Sie der beste Weg, um diese Geschäftslogik zu verwalten, wobei Sie sich auf den saubereren, entkoppelten und testbaren Code konzentrieren? Ihr Feedback und Ihre guten Praktiken? Hast du konkrete Beispiele?

Hauptressourcen:

53
Koryonik

Ich finde Lösung 1) als am einfachsten zu pflegen aus längerer Perspektive. Lösung 2 führt die aufgeblähte "Manager" -Klasse, die schließlich in kleinere Brocken zerlegt wird. 

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

"Zu viele Service-Klassen in einer großen Anwendung" ist kein Grund, SRP zu vermeiden. 

In Bezug auf die Domänensprache finde ich den folgenden Code ähnlich: 

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

und

$group->removeRole($role);

Nach dem, was Sie beschrieben haben, erfordert das Entfernen/Hinzufügen einer Rolle aus einer Gruppe viele Abhängigkeiten (Prinzip der Abhängigkeitsinversion), und dies kann bei einem FAT/aufgeblähten Manager schwierig sein. 

Lösung 3) sieht ähnlich aus wie 1) - Jeder Abonnent wird tatsächlich automatisch im Hintergrund von Entity Manager ausgelöst und kann in einfacheren Szenarien funktionieren. Probleme treten jedoch auf, sobald die Aktion (Hinzufügen/Entfernen einer Rolle) viel Kontext erfordert z.B. welcher Benutzer die Aktion ausgeführt hat, von welcher Seite oder von einer anderen Art komplexer Validierung. 

3
Tomas Dermisek

Siehe hier: Sf2: Verwendung eines Services innerhalb einer Entität

Vielleicht hilft meine Antwort hier. Es wird nur Folgendes angesprochen: Wie entkoppeln Sie Modell vs.

In Ihrer spezifischen Frage würde ich sagen, dass es hier einen "Trick" gibt ... was ist eine "Gruppe"? Es "allein"? oder wenn es sich auf jemanden bezieht?

Anfangs könnten Ihre Model-Klassen wahrscheinlich so aussehen:

UserManager (service, entry point for all others)

Users
User
Groups
Group
Roles
Role

UserManager verfügt über Methoden zum Abrufen der Modellobjekte (wie in dieser Antwort erwähnt, sollten Sie niemals eine new ausführen). In einem Controller können Sie Folgendes tun:

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

Dann kann ... User, wie Sie sagen, Rollen haben, die zugewiesen werden können oder nicht.

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

Ich habe es vereinfacht, man könnte natürlich nach ID hinzufügen, nach Objekt hinzufügen usw.

Aber wenn Sie dies in "natürlicher Sprache" denken ... mal sehen ...

  1. Ich weiß, dass Alice zu einem Fotografen gehört.
  2. Ich bekomme Alice ein Objekt.
  3. Ich frage Alice nach den Gruppen. Ich bekomme die Gruppe Fotografen.
  4. Ich frage Fotografen nach den Rollen.

Sehen Sie mehr im Detail:

  1. Ich weiß, dass Alice die Benutzer-ID = 33 ist und dass sie sich in der Gruppe des Fotografen befindet.
  2. Ich fordere Alice über $user = $manager->getUserById( 33 ); an den UserManager an.
  3. Ich betrete die Gruppe Fotografen durch Alice, vielleicht mit `$ group = $ user-> getGroupByName ('Photographers');
  4. Ich möchte dann die Rollen der Gruppe sehen ... Was soll ich tun?
    • Option 1: $ group-> getRoles ();
    • Option 2: $ group-> getRolesForUser ($ userId);

Die zweite ist wie überflüssig, als ich die Gruppe durch Alice bekam. Sie können eine neue Klasse GroupSpecificToUser erstellen, die von Group erbt.

Ähnlich wie bei einem Spiel ... Was ist ein Spiel? Das "Spiel" als "Schach" im Allgemeinen? Oder das spezifische "Spiel" von "Schach", das Sie und ich gestern begonnen haben?

In diesem Fall würde $user->getGroups() eine Auflistung von GroupSpecificToUser-Objekten zurückgeben.

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

Dieser zweite Ansatz ermöglicht es Ihnen, viele andere Dinge, die früher oder später auftauchen, dort zu kapseln: Darf dieser Benutzer hier etwas tun? Sie können einfach die Gruppenklasse abfragen: $group->allowedToPost();, $group->allowedToChangeName();, $group->allowedToUploadImage(); usw.

In jedem Fall können Sie die Erstellung einer seltsamen Klasse vermeiden und den Benutzer nach diesen Informationen fragen, beispielsweise mit einem $user->getRolesForGroup( $groupId );-Ansatz.

Modell ist keine Persistenzschicht

Ich mag es, wenn ich beim Entwerfen die Perücke vergesse. Normalerweise sitze ich bei meinem Team (oder bei mir selbst für persönliche Projekte) und verbringe 4 oder 6 Stunden damit, darüber nachzudenken, bevor ich eine Codezeile schreibe. Wir schreiben eine API in ein TXT-Dokument. Dann iterieren Sie das Hinzufügen, Entfernen von Methoden usw.

Eine mögliche "Startpunkt" -API für Ihr Beispiel könnte Abfragen von etwas enthalten, beispielsweise einem Dreieck:

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.

Veranstaltungen

Wie im spitzen Artikel gesagt, würde ich auch Ereignisse in das Modell werfen,

Beim Entfernen einer Rolle aus einem Benutzer in einer Gruppe konnte ich beispielsweise in einem "Listener" feststellen, dass, wenn dies der letzte Administrator war, ich a) das Löschen der Rolle abbrechen kann, b) zulassen und die Gruppe ohne verlassen kann Administrator, c) Erlauben Sie es, wählen Sie jedoch einen neuen Administrator mit den Benutzern in der Gruppe usw. oder einer anderen für Sie geeigneten Richtlinie.

Auf dieselbe Weise kann ein Benutzer möglicherweise nur zu 50 Gruppen gehören (wie bei LinkedIn). Sie können dann einfach ein preAddUserToGroup -Ereignis auslösen, und jeder Catcher könnte die Regel enthalten, dass das verboten wird, wenn der Benutzer der Gruppe 51 beitreten möchte.

Diese "Regel" kann eindeutig außerhalb der Klasse "Benutzer", "Gruppe" und "Rolle" liegen und in einer übergeordneten Klasse abgelegt werden, die die "Regeln" enthält, nach denen Benutzer Gruppen beitreten oder diese verlassen können.

Ich empfehle dringend, die andere Antwort zu sehen.

Hoffe zu helfen!

Xavi.

5
Xavi Montero

Als persönliche Präferenz möchte ich einfach anfangen und mit der Anwendung weiterer Geschäftsregeln wachsen. Daher neige ich dazu, zu bevorzugen, dass die Hörer besser vorgehen

Sie gerade 

  • addiere mehr Zuhörer, wenn sich die Geschäftsregeln entwickeln
  • jeweils mit Einzelverantwortung
  • und Sie können diese Listener unabhängig voneinander testen

Etwas, das viele Mocks/Stubs erfordern würde, wenn Sie eine einzelne Service-Klasse haben, z.

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

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
2
jorrel

Ich bin für geschäftsbewusste Entitäten. Doctrine ist sehr bemüht, Ihr Modell nicht mit Infrastrukturproblemen zu belasten. Es verwendet Reflektion, sodass Sie Accessors beliebig ändern können. Die 2 "Doctrine" -Dinge, die in Ihren Entitätsklassen verbleiben können, sind Annotationen (Sie können dank YML-Mapping vermeiden) und die ArrayCollection. Dies ist eine Bibliothek außerhalb von Doctrine ORM (̀Doctrine/Common), daher keine Probleme.

Wenn Sie sich an die Grundlagen von DDD halten, sind Entitäten wirklich der Ort, an dem Sie Ihre Domänenlogik einsetzen können. Natürlich reicht dies manchmal nicht aus. Dann können Sie Domänendienste , Dienste ohne Infrastrukturprobleme hinzufügen.

Lehre Repositorys sind eher Mittelweg: Ich ziehe es vor, diese als einzige Möglichkeit zur Abfrage von Entitäten aufzubewahren, falls sie nicht an dem ursprünglichen Repository-Muster hängen bleiben und ich die generierten Methoden lieber entfernen möchte. Das Hinzufügen von manager service, um alle Abruf-/Speicheroperationen einer bestimmten Klasse zu kapseln, war vor einigen Jahren eine gängige Symfony-Praxis. Ich mag es nicht.

Nach meiner Erfahrung haben Sie möglicherweise viel mehr Probleme mit der Symfony-Formularkomponente. Ich weiß nicht, ob Sie sie verwenden. Sie beschränken Ihre Fähigkeit, den Konstruktor anzupassen, ernsthaft, und Sie können stattdessen benannte Konstruktoren verwenden. Durch das Hinzufügen des PhpDoc @deprecated̀-Tags erhalten Ihre Paare visuelles Feedback. Sie sollten den ursprünglichen Konstruktor nicht verklagen.

Zu guter Letzt werden Sie schließlich beißen, wenn Sie sich zu sehr auf Doktrinereignisse verlassen. Es gibt zu viele technische Einschränkungen, und ich finde, dass diese schwer zu verfolgen sind. Bei Bedarf füge ich domain events vom Controller/Befehl an den Symfony Event Dispatcher ab.

0
romaricdrigon

Ich würde in Betracht ziehen, eine Service-Schicht neben den Entitäten selbst zu verwenden. Entitätsklassen sollten die Datenstrukturen und möglicherweise einige andere einfache Berechnungen beschreiben. Komplexe Regeln gehen an Dienste.

Solange Sie Dienste verwenden, können Sie entkoppelte Systeme, Dienste usw. erstellen. Sie können den Vorteil der Abhängigkeitseinspritzung nutzen und Ereignisse (Dispatcher und Listener) nutzen, um die Kommunikation zwischen den Diensten so zu gestalten, dass sie schwach gekoppelt bleiben.

Ich sage das aufgrund meiner eigenen Erfahrung. Am Anfang habe ich die gesamte Logik in die Entitätsklassen eingefügt (vor allem, wenn ich Symfony 1.x/Doctrine 1.x-Anwendungen entwickelte). So lange die Anwendungen wuchsen, war es sehr schwer zu warten. 

0
Omar Alves