Domain-gesteuertes Design (kurz DDD) ist keine Technologie oder Methodik. DDD bietet eine Struktur von Praktiken und Terminologie, um Entwurfsentscheidungen zu treffen, die Softwareprojekte fokussieren und beschleunigen, die sich mit komplizierten Domänen befassen. Wie beschrieben von Eric Evans und Martin Fowler, Domain-Objekte sind ein Ort, an dem Validierungsregeln und Geschäftslogik platziert werden können.
Eric Evans:
Domänenschicht (oder Modellschicht): Verantwortlich für die Darstellung von Geschäftskonzepten, Informationen zur Geschäftslage und Geschäftsregeln. Der Zustand, der die Geschäftslage widerspiegelt, wird hier kontrolliert und verwendet, obwohl die technischen Details der Speicherung an die Infrastruktur delegiert werden. Diese Schicht ist das Herzstück von Unternehmenssoftware.
Martin Fowler:
Die Logik, die in einem Domänenobjekt enthalten sein sollte, ist die Domänenlogik - Validierungen, Berechnungen, Geschäftsregeln - wie auch immer Sie es nennen möchten.
Wenn Sie die gesamte Validierung in Domänenobjekte einfügen, können Sie mit riesigen und komplexen Domänenobjekten arbeiten. Persönlich bevorzuge ich die Idee, Domänenvalidierungen in separate Validatorkomponenten zu entkoppeln, die jederzeit wiederverwendet werden können und auf Kontext und Benutzeraktion basieren.
Wie Martin Fowler in einem großartigen Artikel schrieb: ContextualValidation .
Ich sehe, dass Menschen häufig Validierungsroutinen für Objekte entwickeln. Diese Routinen kommen auf verschiedene Arten vor, sie können sich im Objekt oder extern befinden, sie können einen Booleschen Wert zurückgeben oder eine Ausnahme auslösen, um einen Fehler anzuzeigen. Eine Sache, von der ich denke, dass sie Menschen ständig auslöst, ist, wenn sie kontextunabhängig an die Objektvalidität denken, wie es eine isValid-Methode impliziert. […] Ich denke, es ist viel nützlicher, sich Validierung als etwas vorzustellen, das an einen Kontext gebunden ist, normalerweise eine Aktion, die Sie ausführen möchten. Fragen Sie beispielsweise, ob diese Bestellung gültig ist, um ausgeführt zu werden, oder ob dieser Kunde gültig ist, um im Hotel einzuchecken. Anstatt Methoden wie isValid zu haben, sollten Sie Methoden wie isValidForCheckIn verwenden.
In diesem Artikel implementieren wir eine einfache Schnittstelle ItemValidator, für die Sie eine implementieren müssen bestätigen Methode mit Rückgabetyp ValidationResult . ValidationResult ist ein Objekt, das das validierte Element und auch das enthält Mitteilungen Objekt. Letzteres enthält eine Ansammlung von Fehlern, Warnungen und Informationsüberprüfungszuständen (Nachrichten), die vom Ausführungskontext abhängen.
Validatoren sind entkoppelte Komponenten, die überall dort wiederverwendet werden können, wo sie benötigt werden. Mit diesem Ansatz können alle Abhängigkeiten, die für Validierungsprüfungen benötigt werden, einfach eingefügt werden. Um beispielsweise in der Datenbank zu überprüfen, ob ein Benutzer mit einer bestimmten E-Mail-Adresse vorhanden ist, wird nur UserDomainService verwendet.
Die Entkopplung der Validatoren erfolgt pro Kontext (Aktion). Wenn die UserCreate-Aktion und die UserUpdate-Aktion Komponenten oder eine andere Aktion (UserActivate, UserDelete, AdCampaignLaunch usw.) entkoppelt haben, kann die Validierung durchgeführt werden schnell wachsen .
Jeder Aktionsvalidator sollte über ein entsprechendes Aktionsmodell verfügen, das nur die zulässigen Aktionsfelder enthält. Zum Erstellen von Benutzern werden folgende Felder benötigt:
UserCreateModel:
{ 'firstName': 'John', 'lastName': 'Doe', 'email': ' [email protected] ', 'password': 'MTIzNDU=' }
Und um den Benutzer zu aktualisieren, sind die folgenden zulässig externalId , Vorname und Familienname, Nachname . externalId wird zur Benutzeridentifikation und nur zum Ändern von verwendet Vorname und Familienname, Nachname ist erlaubt.
UserUpdateModel:
{ 'externalId': 'a55ccd60-9d82-11e5-9f52-0002a5d5c51b', 'firstName': 'John Updated', 'lastName': 'Doe Updated' }
Feldintegritätsprüfungen können gemeinsam genutzt werden. Die maximale Länge von Vorname beträgt immer 255 Zeichen.
Während der Validierung ist es wünschenswert, nicht nur den ersten aufgetretenen Fehler zu erhalten, sondern auch eine Liste aller aufgetretenen Probleme. Beispielsweise können die folgenden 3 Probleme gleichzeitig auftreten und während der Ausführung entsprechend gemeldet werden:
Um diese Art der Validierung zu erreichen, wird zu diesem Zweck so etwas wie ein Validierungsstatus-Builder benötigt Mitteilungen ist vorgestellt. Mitteilungen ist ein Konzept, das ich vor Jahren von meinem großartigen Mentor gehört habe, als er es einführte, um die Validierung zu unterstützen, und auch für verschiedene andere Dinge, die damit gemacht werden können, da Nachrichten nicht nur zur Validierung dienen.
Beachten Sie, dass wir in den folgenden Abschnitten Scala verwenden, um die Implementierung zu veranschaulichen. Nur für den Fall, dass Sie kein sind Scala-Experte Fürchte dich nicht, denn es sollte trotzdem leicht sein, mitzumachen.
Mitteilungen ist ein Objekt, das den Builder für Validierungsstatus darstellt. Es bietet eine einfache Möglichkeit, Fehler, Warnungen und Informationsmeldungen während der Validierung zu erfassen. Jeder Mitteilungen Objekt hat eine innere Sammlung von Botschaft Objekte und kann auch einen Verweis auf haben parentMessages Objekt.
Ein Nachrichtenobjekt ist ein Objekt, das haben kann Art , Nachrichtentext , Schlüssel (Dies ist optional und wird verwendet, um die Validierung bestimmter Eingaben zu unterstützen, die durch die Kennung identifiziert werden.) und schließlich childMessages Dies ist eine großartige Möglichkeit, zusammensetzbare Nachrichtenbäume zu erstellen.
Eine Nachricht kann einen der folgenden Typen haben:
Auf diese Weise strukturierte Nachrichten ermöglichen es uns, sie iterativ zu erstellen und Entscheidungen über die nächsten Aktionen auf der Grundlage des vorherigen Nachrichtenstatus zu treffen. Beispiel: Durchführen einer Validierung während der Benutzererstellung:
@Component class UserCreateValidator @Autowired (private val entityDomainService: UserDomainService) extends ItemValidator[UserCreateEntity] { Asserts.argumentIsNotNull(entityDomainService) private val MAX_ALLOWED_LENGTH = 80 private val MAX_ALLOWED_CHARACTER_ERROR = s'must be less than or equal to $MAX_ALLOWED_LENGTH character' override def validate(item: UserCreateEntity): ValidationResult[UserCreateEntity] = { Asserts.argumentIsNotNull(item) val validationMessages = Messages.of validateFirstName (item, validationMessages) validateLastName (item, validationMessages) validateEmail (item, validationMessages) validateUserName (item, validationMessages) validatePassword (item, validationMessages) ValidationResult( validatedItem = item, messages = validationMessages ) } private def validateFirstName(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.firstName ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.FIRST_NAME_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) } private def validateLastName(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.lastName ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.LAST_NAME_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) } private def validateEmail(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.email ValidateUtils.validateEmail( fieldValue, UserCreateEntity.EMAIL_FORM_ID, localMessages ) ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.EMAIL_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) if(!localMessages.hasErrors()) { val doesExistWithEmail = this.entityDomainService.doesExistByByEmail(fieldValue) ValidateUtils.isFalse( doesExistWithEmail, localMessages, UserCreateEntity.EMAIL_FORM_ID.value, 'User already exists with this email' ) } } private def validateUserName(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.username ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.USERNAME_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) if(!localMessages.hasErrors()) { val doesExistWithUsername = this.entityDomainService.doesExistByUsername(fieldValue) ValidateUtils.isFalse( doesExistWithUsername, localMessages, UserCreateEntity.USERNAME_FORM_ID.value, 'User already exists with this username' ) } } private def validatePassword(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.password ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.PASSWORD_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) } }
Wenn Sie sich diesen Code ansehen, sehen Sie die Verwendung von ValidateUtils. Diese Dienstprogrammfunktionen werden verwendet, um das Messages-Objekt in vordefinierten Fällen zu füllen. Sie können die Implementierung von überprüfen ValidateUtils auf Github Code.
Während der E-Mail-Validierung wird zunächst durch einen Anruf überprüft, ob die E-Mail gültig ist ValidateUtils.validateEmail (… Außerdem wird durch einen Anruf überprüft, ob die E-Mail-Adresse eine gültige Länge hat ValidateUtils.validateLengthIsLessThanOrEqual (… . Sobald diese beiden Überprüfungen abgeschlossen sind, wird überprüft, ob die E-Mail bereits einem Benutzer zugewiesen ist, nur wenn die vorherigen E-Mail-Überprüfungsbedingungen erfüllt sind und dies erledigt ist if (! localMessages.hasErrors ()) {… . Auf diese Weise können teure Datenbankaufrufe vermieden werden. Dies ist nur ein Teil von UserCreateValidator. Den vollständigen Quellcode finden Sie Hier .
Beachten Sie, dass einer der Validierungsparameter auffällt: UserCreateEntity.EMAIL_FORM_ID . Dieser Parameter verbindet den Validierungsstatus mit einer bestimmten Eingabe-ID.
In den vorherigen Beispielen wird die nächste Aktion basierend auf der Tatsache entschieden, ob Mitteilungen Objekt hat Fehler (mit der Methode hasErrors). Man kann leicht überprüfen, ob es 'WARNING' -Meldungen gibt, und es bei Bedarf erneut versuchen.
Eine Sache, die bemerkt werden kann, ist der Weg localMessages wird genutzt. Lokale Nachrichten sind Nachrichten, die wie jede Nachricht erstellt wurden, jedoch mit parentMessages. Vor diesem Hintergrund besteht das Ziel darin, nur auf den aktuellen Validierungsstatus (in diesem Beispiel emailValidation) zu verweisen localMessages.hasErrors kann aufgerufen werden, wobei es nur überprüft wird, wenn der E-Mail-Validierungskontext Fehler enthält. Wenn eine Nachricht zu localMessages hinzugefügt wird, wird sie auch zu parentMessages hinzugefügt, sodass alle Validierungsnachrichten in einem höheren Kontext von UserCreateValidation vorhanden sind.
Nachdem wir Nachrichten in Aktion gesehen haben, konzentrieren wir uns im nächsten Kapitel auf ItemValidator.
ItemValidator ist eine einfache Eigenschaft (Schnittstelle), die Entwickler zur Implementierung der Methode zwingt bestätigen , die ValidationResult zurückgeben muss.
ItemValidator:
trait ItemValidator[T] { def validate(item:T) : ValidationResult[T] }
ValidationResult:
case class ValidationResult[T: Writes]( validatedItem : T, messages : Messages ) { Asserts.argumentIsNotNull(validatedItem) Asserts.argumentIsNotNull(messages) def isValid :Boolean = { !messages.hasErrors } def errorsRestResponse = { Asserts.argumentIsTrue(!this.isValid) ResponseTools.of( data = Some(this.validatedItem), messages = Some(messages) ) } }
Wenn ItemValidators wie UserCreateValidator umgesetzt werden, um zu sein Abhängigkeitsspritze Komponenten, dann können ItemValidator-Objekte in jedes Objekt eingefügt und wiederverwendet werden, das eine UserCreate-Aktionsüberprüfung benötigt.
Nachdem die Validierung ausgeführt wurde, wird überprüft, ob die Validierung erfolgreich war. Wenn dies der Fall ist, bleiben die Benutzerdaten in der Datenbank erhalten. Andernfalls wird die API-Antwort mit Validierungsfehlern zurückgegeben.
Im nächsten Abschnitt werden wir sehen, wie wir Validierungsfehler in der RESTful-API-Antwort darstellen können und wie wir mit API-Verbrauchern über Ausführungsaktionszustände kommunizieren können.
Nach erfolgreicher Validierung der Benutzeraktion, in unserem Fall der Benutzererstellung, müssen die Ergebnisse der Validierungsaktion dem RESTful API-Konsumenten angezeigt werden. Der beste Weg ist eine einheitliche API-Antwort, bei der nur der Kontext gewechselt wird (in Bezug auf JSON, Wert von 'Daten'). Mit einheitlichen Antworten können Fehler RESTful API-Verbrauchern problemlos präsentiert werden.
Einheitliche Antwortstruktur:
{ 'messages' : { 'global' : { 'info': [], 'warnings': [], 'errors': [] }, 'local' : [] }, 'data':{} }
Die einheitliche Antwort besteht aus zwei Ebenen von Nachrichten: global und lokal. Lokale Nachrichten sind Nachrichten, die an bestimmte Eingänge gekoppelt sind. Beispiel: 'Benutzername ist zu lang, höchstens 80 Zeichen sind zulässig' _. Globale Nachrichten sind Nachrichten, die den Status der gesamten Daten auf der Seite widerspiegeln, z. B. 'Benutzer ist erst nach Genehmigung aktiv'. Lokale und globale Nachrichten haben drei Ebenen - Fehler, Warnung und Informationen. Der Wert von „Daten“ ist kontextspezifisch. Beim Erstellen von Benutzern enthält das Datenfeld Benutzerdaten. Beim Abrufen einer Benutzerliste ist das Datenfeld jedoch ein Array von Benutzern.
Mit dieser Art von strukturierter Antwort kann der Client-UI-Handler einfach erstellt werden, der für die Anzeige von Fehlern, Warnungen und Informationsmeldungen verantwortlich ist. Globale Nachrichten werden oben auf der Seite angezeigt, da sie sich auf den globalen API-Aktionsstatus beziehen und lokale Nachrichten in der Nähe der angegebenen Eingabe (Feld) angezeigt werden können, da sie in direktem Zusammenhang mit dem Feldwert stehen. Fehlermeldungen können in roter Farbe, Warnmeldungen in gelb und Informationen in blau angezeigt werden.
In einer AngularJS-basierten Client-App können beispielsweise zwei Anweisungen für die Verarbeitung lokaler und globaler Antwortnachrichten verantwortlich sein, sodass nur diese beiden Handler alle Antworten auf konsistente Weise verarbeiten können.
Die Anweisung für die lokale Nachricht muss auf ein übergeordnetes Element des tatsächlichen Elements angewendet werden, das alle Nachrichten enthält.
localmessages.direcitive.js ::
(function() { 'use strict'; angular .module('reactiveClient') .directive('localMessagesValidationDirective', localMessagesValidationDirective); /** @ngInject */ function localMessagesValidationDirective(_) { return { restrict: 'AE', transclude: true, scope: { binder: '=' }, template: ' ', link: function (scope, element) { var messagesWatchCleanUp = scope.$watch('binder', messagesBinderWatchCallback); scope.$on('$destroy', function() { messagesWatchCleanUp(); }); function messagesBinderWatchCallback (messagesResponse) { if (messagesResponse != undefined && messagesResponse.messages != undefined) { if (messagesResponse.messages.local.length > 0) { element.find('.alert').remove(); _.forEach(messagesResponse.messages.local, function (localMsg) { var selector = element.find('[id='' + localMsg.inputId + '']').parent(); _.forEach(localMsg.info, function (msg) { var infoMsg = ' ×' + msg + ' '; selector.after(infoMsg); }); _.forEach(localMsg.warnings, function (msg) { var warningMsg = ' ×' + msg + ' '; selector.after(warningMsg); }); _.forEach(localMsg.errors, function (msg) { var errorMsg = ' ×' + msg + ' '; selector.after(errorMsg); }); }); } } } } } } })();
Die Direktive für globale Nachrichten wird in das Stammlayoutdokument (index.html) aufgenommen und bei einem Ereignis registriert, um alle globalen Nachrichten zu verarbeiten.
globalmessages.directive.js ::
(function() { 'use strict'; angular .module('reactiveClient') .directive('globalMessagesValidationDirective', globalMessagesValidationDirective); /** @ngInject */ function globalMessagesValidationDirective(_, toastr, $rootScope, $log) { return { restrict: 'AE', link: function (scope) { var cleanUpListener = $rootScope.$on('globalMessages', globalMessagesWatchCallback); scope.$on('$destroy', function() { cleanUpListener(); }); function globalMessagesWatchCallback (event, messagesResponse) { $log.log('received rootScope event: ' + event); if (messagesResponse != undefined && messagesResponse.messages != undefined) { if (messagesResponse.messages.global != undefined) { _.forEach(messagesResponse.messages.global.info, function (msg) { toastr.info(msg); }); _.forEach(messagesResponse.messages.global.warnings, function (msg) { toastr.warning(msg); }); _.forEach(messagesResponse.messages.global.errors, function (msg) { toastr.error(msg); }); } } } } } } })();
Betrachten wir für ein vollständigeres Beispiel die folgende Antwort, die eine lokale Nachricht enthält:
{ 'messages' : { 'global' : { 'info': [], 'warnings': [], 'errors': [] }, 'local' : [ { 'inputId' : 'email', 'errors' : ['User already exists with this email'], 'warnings' : [], 'info' : [] } ] }, 'data':{ 'firstName': 'John', 'lastName': 'Doe', 'email': ' [email protected] ', 'password': 'MTIzNDU=' } }
Die obige Antwort kann zu folgenden Ergebnissen führen:
Auf der anderen Seite mit einer globalen Nachricht als Antwort:
{ 'messages' : { 'global' : { 'info': ['User successfully created.'], 'warnings': ['User will not be available for login until is activated'], 'errors': [] }, 'local' : [] }, 'data':{ 'externalId': 'a55ccd60-9d82-11e5-9f52-0002a5d5c51b', 'firstName': 'John', 'lastName': 'Doe', 'email': ' [email protected] ' } }
Die Client-App kann die Nachricht jetzt deutlicher anzeigen:
In den obigen Beispielen ist zu sehen, wie eine einheitliche Antwortstruktur für jede Anforderung mit demselben Handler behandelt werden kann.
Das Anwenden der Validierung auf große Projekte kann verwirrend werden, und Validierungsregeln finden Sie überall im Projektcode. Wenn die Validierung konsistent und gut strukturiert bleibt, werden die Dinge einfacher und wiederverwendbarer.
Diese Ideen finden Sie in zwei verschiedenen Versionen von Boilerplates:
In diesem Artikel habe ich meine Vorschläge zur Unterstützung einer umfassenden, zusammensetzbaren Kontextvalidierung vorgestellt, die einem Benutzer leicht präsentiert werden kann. Ich hoffe, dies wird Ihnen helfen, die Herausforderungen einer ordnungsgemäßen Validierung und Fehlerbehandlung ein für alle Mal zu lösen. Bitte hinterlassen Sie Ihre Kommentare und teilen Sie Ihre Gedanken unten.