Domain Validation in a CQRS architecture

Gefahr ... Gefahr Dr. Smith ... Philosophischer Posten voraus

Der Zweck dieses Beitrags besteht darin, festzustellen, ob das Platzieren der Überprüfungslogik außerhalb meiner Domänenentitäten (Gesamtstamm tatsächlich) mir tatsächlich mehr Flexibilität gewährt oder ob dies der Fall istKamikaze-Code

Grundsätzlich möchte ich wissen, ob es einen besseren Weg gibt, meine Domain-Entitäten zu validieren. So habe ich das vor, aber ich hätte gerne Ihre Meinung dazu

Der erste Ansatz, über den ich nachdachte, war:

class Customer : EntityBase<Customer>
{
   public void ChangeEmail(string email)
   {
      if(string.IsNullOrWhitespace(email))   throw new DomainException(“...”);
      if(!email.IsEmail())  throw new DomainException();
      if(email.Contains(“@mailinator.com”))  throw new DomainException();
   }
}

Eigentlich mag ich diese Überprüfung nicht, weil selbst wenn ich die Überprüfungslogik in die richtige Entität einkapsle, dies gegen das Open / Close-Prinzip verstößt (Open for Extension, Close for Modification) und ich habe festgestellt, dass die Codewartung gegen dieses Prinzip verstößt ein wahrer schmerz, wenn die anwendung komplexer wird. Warum? Denn Domain-Regeln ändern sich häufiger, als wir zugeben möchten, und wenn die Regeln es sindversteckt und eingebettet In einer Entität wie dieser sind sie schwer zu testen, schwer zu lesen, schwer zu warten, aber der wahre Grund, warum mir dieser Ansatz nicht gefällt, ist: Wenn sich die Validierungsregeln ändern, muss ich meine Domain-Entität bearbeiten. Dies war ein sehr einfaches Beispiel, aber in RL könnte die Validierung komplexer sein

Nach der Philosophie von Udi DahanRollen explizit machenNach der Empfehlung von Eric Evans im Blue Book war der nächste Versuch, das Spezifikationsmuster zu implementieren

class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
   private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
   public bool IsSatisfiedBy(Customer customer)
   {
      return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
   }
}

Aber dann erkenne ich, dass ich, um diesem Ansatz zu folgen, zuerst meine Entitäten mutieren musste, um das zu bestehenWert wird bewertetIn diesem Fall würde die E-Mail, wenn sie jedoch mutiert wird, dazu führen, dass meine Domain-Ereignisse ausgelöst werden, was ich erst dann möchte, wenn die neue E-Mail gültig ist

Nachdem ich diese Ansätze bedacht hatte, kam ich zu diesem Ansatz, da ich eine CQRS-Architektur implementieren werde:

class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
   public void IsValid(Customer entity, ChangeEmailCommand command)
   {
      if(!command.Email.HasValidDomain())  throw new DomainException(“...”);
   }
}

Nun, das ist die Hauptidee, die Entität wird an den Validator übergeben, falls wir einen Wert von der Entität benötigen, um die Validierung durchzuführen. Der Befehl enthält die vom Benutzer stammenden Daten und da die Validatoren berücksichtigt werdeninjizierbar Objekte, denen externe Abhängigkeiten hinzugefügt werden könnten, wenn die Validierung dies erfordert.

Nun das DilemmaIch bin mit einem Design wie diesem zufrieden, da meine Validierung in einzelne Objekte eingekapselt ist, was viele Vorteile mit sich bringt: einfacher Komponententest, einfach zu warten, Domäneninvarianten werden explizit in der Ubiquitous Language ausgedrückt, einfach zu erweitern, die Validierungslogik ist zentralisiert und die Validatoren können zusammen verwendet werden, um komplexe Domänenregeln durchzusetzen. Und selbst wenn ich weiß, dass ich die Validierung meiner Entitäten außerhalb von ihnen platziere (Sie könnten einen Codegeruch argumentieren - anämische Domäne), denke ich, dass der Kompromiss akzeptabel ist

Aber es gibt eine Sache, bei der ich nicht herausgefunden habe, wie man sie sauber umsetzt.Wie soll ich diese Komponenten verwenden ...

Da sie injiziert werden, passen sie nicht auf natürliche Weise in meine Domain-Entitäten, sodass ich grundsätzlich zwei Optionen sehe:

Übergeben Sie die Validatoren an jede Methode meiner Entität

Validiere meine Objekte extern (aus dem Command Handler)

Ich bin mit Option 1 nicht zufrieden, daher würde ich erklären, wie ich es mit Option 2 machen würde

class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
   // here I would get the validators required for this command injected
   private IEnumerable<IDomainInvariantValidator> validators;
   public void Execute(ChangeEmailCommand command)
   {
      using (var t = this.unitOfWork.BeginTransaction())
      {
         var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
         // here I would validate them, something like this
         this.validators.ForEach(x =. x.IsValid(customer, command));
         // here I know the command is valid
         // the call to ChangeEmail will fire domain events as needed
         customer.ChangeEmail(command.Email);
         t.Commit();
      }
   }
}

Nun das ist es. Können Sie mir Ihre Gedanken dazu mitteilen oder Ihre Erfahrungen mit der Validierung von Domain-Entitäten mitteilen?

BEARBEITEN

Ich denke, es ist nicht klar aus meiner Frage, aber das eigentliche Problem ist: Das Ausblenden der Domain-Regeln hat schwerwiegende Auswirkungen auf die zukünftige Wartbarkeit der Anwendung, und auch Domain-Regeln ändern sich häufig während des Lebenszyklus der App. Wenn wir sie daher in diesem Sinne implementieren, können wir sie leicht erweitern. Stellen Sie sich jetzt vor, dass in Zukunft eine Regel-Engine implementiert wird. Wenn die Regeln außerhalb der Domänen-Entitäten gekapselt werden, ist diese Änderung einfacher zu implementieren

Mir ist bewusst, dass das Platzieren der Validierung außerhalb meiner Entitäten die in seiner Antwort @jgauffin erwähnte Kapselung bricht, aber ich denke, dass die Vorteile des Platzierens der Validierung in einzelnen Objekten wesentlich größer sind, als nur die Kapselung einer Entität beizubehalten. Jetzt denke ich, dass die Kapselung in einer traditionellen n-Tier-Architektur sinnvoller ist, da die Entitäten an mehreren Stellen der Domänenschicht verwendet wurden. In einer CQRS-Architektur gibt es jedoch einen Befehlshandler, der auf ein aggregiertes Stammverzeichnis und zugreift, wenn ein Befehl eingeht Durch Ausführen von Vorgängen für die Gesamtwurzel wird nur ein perfektes Fenster zum Platzieren der Validierung erstellt.

Ich möchte einen kleinen Vergleich zwischen den Vorteilen der Platzierung der Validierung in einer Entität und der Platzierung in einzelnen Objekten anstellen

Validierung in einzelnen Objekten

Profi. Einfach zu schreibenProfi. Einfach zu testenProfi. Es ist ausdrücklich zum Ausdruck gebrachtProfi. Es wird Teil des Domain-Designs, ausgedrückt in der aktuellen Ubiquitous LanguageProfi. Da es nun Teil des Designs ist, kann es mithilfe von UML-Diagrammen modelliert werdenProfi. Extrem pflegeleichtProfi. Macht meine Entitäten und die Validierungslogik lose gekoppeltProfi. Einfach zu erweiternProfi. Nach dem SRPProfi. Nach dem Open / Close-PrinzipProfi. Nicht gegen das Gesetz von Demeter (mmm) verstoßen?Profi. Ich bin zentralisiertProfi. Es könnte wiederverwendbar seinProfi. Bei Bedarf können externe Abhängigkeiten einfach eingefügt werdenProfi. Wenn Sie ein Plug-in-Modell verwenden, können Sie neue Validatoren hinzufügen, indem Sie die neuen Assemblys löschen, ohne die gesamte Anwendung neu kompilieren zu müssenProfi. Das Implementieren einer Regelengine wäre einfacherCon. Kapselung brechenCon. Wenn die Kapselung obligatorisch ist, müssten wir die einzelnen Validatoren an die Entitätsmethode (Aggregatmethode) übergeben

Validierung in der Entität gekapselt

Profi. Gekapselt?Profi. Wiederverwendbar?

Ich würde gerne Ihre Gedanken dazu lesen

Antworten auf die Frage(11)

Ihre Antwort auf die Frage