portaldacalheta.pt
  • Principal
  • Gestion De L'ingénierie
  • Conseils Et Outils
  • Rise Of Remote
  • Agile
Back-End

Validation du contexte dans la conception pilotée par domaine



La conception axée sur le domaine (DDD en bref) n'est ni une technologie ni une méthodologie. DDD fournit une structure de pratiques et de terminologie pour prendre des décisions de conception qui concentrent et accélèrent les projets logiciels traitant de domaines complexes. Comme décrit par Eric Evans et Martin Fowler, les objets de domaine sont un endroit où placer les règles de validation et la logique métier.

Eric Evans:



Couche de domaine (ou couche de modèle): responsable de la représentation des concepts de l'entreprise, des informations sur la situation de l'entreprise et des règles commerciales. L'état qui reflète la situation de l'entreprise est ici contrôlé et utilisé, même si les détails techniques de son stockage sont délégués à l'infrastructure. Cette couche est le cœur des logiciels d'entreprise.



Martin Fowler:



La logique qui devrait être dans un objet de domaine est la logique de domaine - validations, calculs, règles métier - comme vous voudrez l'appeler.

Validation du contexte dans la conception pilotée par domaine



Mettre toute la validation dans les objets de domaine entraîne des objets de domaine énormes et complexes avec lesquels travailler. Personnellement, je préfère de loin l'idée de découpler les validations de domaine en composants de validation séparés qui peuvent être réutilisés à tout moment et qui seront basés sur le contexte et l'action de l'utilisateur.

Comme l'écrit Martin Fowler dans un excellent article: ContextualValidation .



Une chose courante que je vois faire est de développer des routines de validation pour les objets. Ces routines se présentent de différentes manières, elles peuvent être dans l'objet ou externes, elles peuvent renvoyer un booléen ou lancer une exception pour indiquer un échec. Une chose qui, à mon avis, dérange constamment les gens, c'est quand ils pensent à la validité des objets d'une manière indépendante du contexte, comme l'implique une méthode isValid. […] Je pense qu’il est beaucoup plus utile de considérer la validation comme quelque chose qui est lié à un contexte, généralement une action que vous voulez faire. Par exemple, demander si cette commande est valide pour être remplie, ou ce client est-il valide pour s'enregistrer à l'hôtel. Donc, plutôt que d'avoir des méthodes comme isValid, utilisez des méthodes comme isValidForCheckIn.

Proposition de validation d'action

Dans cet article, nous allons implémenter une interface simple ItemValidator pour laquelle vous devez implémenter un valider méthode avec type de retour ValidationRésultat . ValidationResult est un objet contenant l'élément qui a été validé ainsi que le messages objet. Ce dernier contient une accumulation d'erreurs, d'avertissements et d'états de validation d'informations (messages) en fonction du contexte d'exécution.



documentation en développement logiciel agile

Les validateurs sont des composants découplés qui peuvent être facilement réutilisés partout où ils sont nécessaires. Avec cette approche, toutes les dépendances, qui sont nécessaires pour les contrôles de validation, peuvent être facilement injectées. Par exemple, pour vérifier dans la base de données s'il existe un utilisateur avec un e-mail donné, seul UserDomainService est utilisé.

Le découplage des validateurs se fera par contexte (action). Ainsi, si l'action UserCreate et l'action UserUpdate ont des composants découplés ou toute autre action (UserActivate, UserDelete, AdCampaignLaunch, etc.), la validation peut grandir rapidement .



Chaque validateur d'action doit avoir un modèle d'action correspondant qui n'aura que les champs d'action autorisés. Pour créer des utilisateurs, les champs suivants sont nécessaires:

UserCreateModel:



{ 'firstName': 'John', 'lastName': 'Doe', 'email': ' [email protected] ', 'password': 'MTIzNDU=' }

Et pour mettre à jour l'utilisateur, les éléments suivants sont autorisés externalId , Prénom et nom de famille . externalId est utilisé pour l'identification de l'utilisateur et uniquement le changement de Prénom et nom de famille est autorisée.

UserUpdateModel:

{ 'externalId': 'a55ccd60-9d82-11e5-9f52-0002a5d5c51b', 'firstName': 'John Updated', 'lastName': 'Doe Updated' }

Les validations d'intégrité des champs peuvent être partagées, la longueur maximale de firstName est toujours de 255 caractères.

Lors de la validation, il est souhaitable non seulement d'obtenir la première erreur qui se produit, mais une liste de tous les problèmes rencontrés. Par exemple, les 3 problèmes suivants peuvent survenir en même temps et peuvent être signalés en conséquence lors de l'exécution:

  • format d'adresse non valide [ERROR]
  • l'e-mail doit être unique parmi les utilisateurs [ERROR]
  • mot de passe trop court [ERROR]

Pour réaliser ce type de validation, quelque chose comme le générateur d'état de validation est nécessaire, et à cette fin messages est introduit. messages C'est un concept que j'ai entendu de mon grand mentor il y a des années lorsqu'il l'a présenté pour soutenir la validation et aussi pour diverses autres choses qui peuvent être faites avec, car les messages ne sont pas uniquement destinés à la validation.

Notez que dans les sections suivantes, nous utiliserons Scala pour illustrer l'implémentation. Juste au cas où tu n'es pas un Expert Scala , ne craignez pas car il devrait être facile à suivre néanmoins.

Messages dans la validation de contexte

messages est un objet qui représente le générateur d'état de validation. Il fournit un moyen simple de collecter les erreurs, les avertissements et les messages d'information lors de la validation. Chaque messages objet a une collection interne de Message objets et peut également avoir une référence à parentMessages objet.

Un objet Message est un objet qui peut avoir type , messageText , clé (qui est facultatif et est utilisé pour prendre en charge la validation d'entrées spécifiques qui sont identifiées par identifiant), et enfin childMessages cela constitue un excellent moyen de créer des arbres de messages composables.

Un message peut être de l'un des types suivants:

  • Information
  • Attention
  • Erreur

Les messages structurés de cette manière nous permettent de les construire de manière itérative et permet également de prendre des décisions concernant les actions suivantes en fonction de l'état des messages précédents. Par exemple, effectuer une validation lors de la création de l'utilisateur:

@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 ) } }

En regardant dans ce code, vous pouvez voir l'utilisation de ValidateUtils. Ces fonctions utilitaires sont utilisées pour remplir l'objet Messages dans des cas prédéfinis. Vous pouvez consulter la mise en œuvre de ValidateUtils sur Github code.

Lors de la validation de l'e-mail, il est d'abord vérifié si l'e-mail est valide en appelant ValidateUtils.validateEmail (… , et il est également vérifié si l'email a une longueur valide en appelant ValidateUtils.validateLengthIsLessThanOrEqual (… . Une fois ces deux validations effectuées, la vérification si l'e-mail est déjà attribué à un utilisateur est effectuée, uniquement si les conditions de validation des e-mails préalables sont satisfaisantes et cela se fait avec if (! localMessages.hasErrors ()) {… . De cette façon, les appels coûteux à la base de données peuvent être évités. Ce n'est qu'une partie de UserCreateValidator. Le code source complet peut être trouvé Ici .

Notez que l'un des paramètres de validation se démarque: UserCreateEntity.EMAIL_FORM_ID . Ce paramètre connecte l'état de validation à un ID d'entrée spécifique.

Dans les exemples précédents, l'action suivante est décidée sur la base du fait que si messages l'objet a des erreurs (en utilisant la méthode hasErrors). On peut facilement vérifier s'il y a des messages «WARNING» et réessayer si nécessaire.

Une chose qui peut être remarquée est la manière localMessages est utilisé. Les messages locaux sont des messages qui ont été créés de la même manière que n'importe quel message, mais avec parentMessages. Cela dit, l'objectif est d'avoir une référence uniquement à l'état de validation actuel (dans cet exemple emailValidation), donc localMessages.hasErrors peut être appelé, où il est vérifié uniquement si le contexte emailValidation hasErrors. De même, lorsqu'un message est ajouté à localMessages, il est également ajouté à parentMessages et donc tous les messages de validation existent dans le contexte supérieur de UserCreateValidation.

Maintenant que nous avons vu Messages en action, dans le prochain chapitre, nous nous concentrerons sur ItemValidator.

ItemValidator - Composant de validation réutilisable

ItemValidator est un trait simple (interface) qui oblige les développeurs à implémenter la méthode valider , qui doit renvoyer ValidationResult.

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

Lorsque ItemValidators tels que UserCreateValidator sont mis en œuvre pour être injection de dépendance composants, puis les objets ItemValidator peuvent être injectés et réutilisés dans tout objet nécessitant la validation de l'action UserCreate.

Une fois la validation exécutée, il est vérifié si la validation a réussi. Si tel est le cas, les données utilisateur sont conservées dans la base de données, mais dans le cas contraire, la réponse de l'API contenant des erreurs de validation est renvoyée.

Dans la section suivante, nous verrons comment nous pouvons présenter les erreurs de validation dans la réponse de l'API RESTful et également comment communiquer avec les consommateurs d'API sur les états des actions d'exécution.

Réponse API unifiée - Interaction utilisateur simple

Une fois l'action de l'utilisateur validée, dans notre cas de création d'utilisateur, les résultats de l'action de validation doivent être affichés au consommateur d'API RESTful. Le meilleur moyen est d'avoir une réponse API unifiée où seul le contexte sera changé (en termes de JSON, valeur de «données»). Avec des réponses unifiées, les erreurs peuvent être facilement présentées aux consommateurs d'API RESTful.

Structure de réponse unifiée:

{ 'messages' : { 'global' : { 'info': [], 'warnings': [], 'errors': [] }, 'local' : [] }, 'data':{} }

La réponse unifiée est structurée pour avoir deux niveaux de messages, global et local. Les messages locaux sont des messages couplés à des entrées spécifiques. Par exemple, 'le nom d'utilisateur est trop long, 80 caractères au maximum sont autorisés' _. Les messages globaux sont des messages qui reflètent l'état de l'ensemble des données sur la page, tels que «l'utilisateur ne sera pas actif tant qu'il n'aura pas été approuvé». Les messages locaux et globaux ont trois niveaux: erreur, avertissement et information. La valeur des «données» est spécifique au contexte. Lors de la création d'utilisateurs, le champ de données contiendra des données utilisateur, mais lors de l'obtention d'une liste d'utilisateurs, le champ de données sera un tableau d'utilisateurs.

Avec ce type de réponse structurée, le gestionnaire de l'interface utilisateur client peut être créé facilement, qui sera responsable de l'affichage des erreurs, des avertissements et des messages d'information. Les messages globaux seront affichés en haut de la page, car ils sont liés à l'état d'action global de l'API, et les messages locaux peuvent être affichés près de l'entrée spécifiée (champ), car ils sont directement liés à la valeur du champ. Les messages d'erreur peuvent être présentés en rouge, les messages d'avertissement en jaune et les informations en bleu.

Par exemple, dans une application client basée sur AngularJS, nous pouvons avoir deux directives responsables de la gestion des messages de réponse locaux et globaux, de sorte que seuls ces deux gestionnaires puissent traiter toutes les réponses de manière cohérente.

La directive pour le message local devra être appliquée à un élément parent de l'élément réel contenant tous les messages.

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

La directive pour les messages globaux sera incluse dans le document de mise en page racine (index.html) et s'inscrira à un événement pour gérer tous les messages globaux.

qu'est-ce qu'un tour vers le bas

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

Pour un exemple plus complet, considérons la réponse suivante contenant un message local:

{ '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=' } }

La réponse ci-dessus peut conduire à quelque chose comme suit:

D'autre part, avec un message global en réponse:

{ '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] ' } }

L'application cliente peut désormais afficher le message avec plus de visibilité:

Dans les exemples ci-dessus, on peut voir comment une structure de réponse unifiée peut être gérée pour toute demande avec le même gestionnaire.

Conclusion

L'application de la validation sur de grands projets peut devenir déroutante et des règles de validation peuvent être trouvées partout dans le code du projet. Garder la validation cohérente et bien structurée rend les choses plus faciles et réutilisables.

Vous pouvez trouver ces idées mises en œuvre dans deux versions différentes de passe-partout ci-dessous:

  • Standard Play 2.3, Scala 2.11.1, Slick 2.1, Postgres 9.1, injection de dépendance à ressort
  • Jeu réactif non bloquant 2.4, Scala 2.11.7, Slick 3.0, Postgres 9.4, Injection de dépendance de guice

Dans cet article, j'ai présenté mes suggestions sur la façon de prendre en charge une validation de contexte profonde et composable qui peut être facilement présentée à un utilisateur. J'espère que cela vous aidera à résoudre les problèmes de validation et de gestion des erreurs une fois pour toutes. N'hésitez pas à laisser vos commentaires et à partager vos réflexions ci-dessous.

Comment créer des animations de chargement personnalisées pour réduire les taux de rebond

Outils Et Tutoriels

Comment créer des animations de chargement personnalisées pour réduire les taux de rebond
Un guide pratique de DesignOps

Un guide pratique de DesignOps

Procédé De Design

Articles Populaires
API de réseaux sociaux: le portail Internet vers le monde réel
API de réseaux sociaux: le portail Internet vers le monde réel
Explorer les raisons de la critique du Design Thinking
Explorer les raisons de la critique du Design Thinking
La concurrence et la tolérance aux pannes simplifiées: un didacticiel Akka avec des exemples
La concurrence et la tolérance aux pannes simplifiées: un didacticiel Akka avec des exemples
Dix fonctionnalités Kotlin pour booster le développement Android
Dix fonctionnalités Kotlin pour booster le développement Android
Soutenir l'offre technologique grâce à l'éducation STEM
Soutenir l'offre technologique grâce à l'éducation STEM
 
État de l'industrie Fintech (avec infographie)
État de l'industrie Fintech (avec infographie)
DevOps: qu'est-ce que c'est et pourquoi c'est important
DevOps: qu'est-ce que c'est et pourquoi c'est important
Scala vs Java: pourquoi devrais-je apprendre Scala?
Scala vs Java: pourquoi devrais-je apprendre Scala?
Qu'est-ce qu'un round down et comment en éviter un
Qu'est-ce qu'un round down et comment en éviter un
Création d'illustrations époustouflantes avec Sketch et Looper en un rien de temps
Création d'illustrations époustouflantes avec Sketch et Looper en un rien de temps
Articles Populaires
  • qui a développé c++ ?
  • cartes de crédit piratées gratuites avec de l'argent dessus 2018
  • générer un document word à partir de xml
  • meilleur endroit pour apprendre c
  • piratage de numéro de carte de crédit paypal
  • qu'est-ce que la crise de la dette grecque
Catégories
  • Gestion De L'ingénierie
  • Conseils Et Outils
  • Rise Of Remote
  • Agile
  • © 2022 | Tous Les Droits Sont Réservés

    portaldacalheta.pt