Maison / Technologie / Commandes et événements au lieu de CRUD – Partie 1: Commandes

Commandes et événements au lieu de CRUD – Partie 1: Commandes

Commandes et événements au lieu de CRUD – Partie 1: Commandes

photo par Daniel Bradley sur Unsplash

De nombreuses applications, en particulier dans le monde de l'entreprise, implémentent la manipulation d'entité principalement basée sur les opérations CRUD. CRUD est un acronyme pour

  • Crecréer
  • Ravant
  • Update
  • elete

Ces opérations semblent correspondre aux opérations d'entité basées sur un formulaire classique: en quelque part dans l'interface utilisateur, vous cliquez sur un bouton «Ajouter», un formulaire vous permet de capturer des données (par exemple, un nouveau contact dans un système CRM), vous cliquez sur enregistrer et sur la nouvelle entité. est créé dans la base de données.

Lorsque vous souhaitez modifier une entité, le formulaire est affiché presque, mais il est pré-rempli avec les données de l’entité de la base de données. Vous pouvez modifier les champs, cliquer sur «Enregistrer» et l’entité modifiée est conservée.

Problèmes typiques du CRUD

Du point de vue de la mise en œuvre dans les deux situations (création et mise à jour), un objet entité de la même classe est envoyé. Même si l'utilisateur ne change que le numéro de téléphone d'un contact, l'entité entière est mise à jour dans notre base de données.

Pour le prototypage rapide et les applications très basiques, cela peut être une bonne approche car elle permet une mise en œuvre rapide. Les opérations CRUD sont supportées nativement par de nombreux frameworks. Ils peuvent être facilement implémentés à partir d'une base de données relationnelle utilisant des couches d'abstraction telles que JPA ou Hibernate. Il existe également des piles technologiques telles que Ruby on Rails ou Grails qui accélèrent la mise en œuvre des opérations CRUD.

Mais dans la plupart des produits, les besoins des entreprises augmentent avec le temps et, comme vous le verrez, CRUD est très limité dans ses capacités. La liste suivante n'est pas complète, mais elle présente les exigences et problèmes classiques que j'ai rencontrés à plusieurs reprises au cours de ma vie de développeur.

Problème 1: Grandes classes Entity avec beaucoup de propriétés facultatives

Les objets métier qui paraissent simples au début du cycle de vie d’un produit croissent avec le temps. Il suffit de regarder un objet de contact dans une application CRM normale: ce qui semble simple et petit au début devient une classe monstre avec des dizaines de propriétés dans le temps, étalant plusieurs onglets dans une interface utilisateur.

Il y a souvent des vues qui ne montrent qu'un sous-ensemble de votre objet entité monstre. Que voulez-vous faire avec les données non pertinentes ici?

  • Voulez-vous inclure tout les données de l'objet sont envoyées à l'interface utilisateur?
  • Voulez-vous créer de nouveaux objets spécialisés de transfert de données?
  • Ou voulez-vous utiliser votre objet entité standard et laisser les champs non obligatoires non définis (autrement dit nuls)?

Cela devient encore plus intéressant si vous souhaitez enregistrer les modifications apportées par l'utilisateur dans le formulaire de saisie: Quels champs souhaitez-vous transférer? Tout ce qui est montré dans le formulaire ou seulement ce que l'utilisateur a édité? Et comment vos services de bas niveau devraient-ils savoir quels champs ils peuvent s’attendre à définir et quels champs peuvent rester vides?

Dans la plupart des applications, j’ai vu un mélange d’objets légèrement spécialisés et de définition de champs sur null. Les développeurs ne voulaient pas créer d’objets spécialisés pour tout cas d'utilisation. En partie parce qu'il n'y avait pas de règle claire quant au moment d'introduire de nouveaux objets de transfert de données et en partie parce qu'ils ne savaient pas comment nommer ces nouvelles classes.

En conséquence, avec le temps, il devient difficile de mettre à jour la couche de service. Il n'y a pas de logique claire quant aux objets de transfert de données utilisés et aux champs des objets utilisés susceptibles d'être définis. Cela conduit à des tonnes de vérifications non nulles dans les langages null-safe ou plus vraisemblablement et même pire à de nombreuses NullPointerExceptions.

Problème 2: Implémenter Annuler

Dans la plupart des produits, plus tôt ou plus tard, le souhait d'annuler les modifications est exprimé. Implémenter cela dans une application basée sur CRUD est au moins difficile.

Problème 3: Fournir un historique des modifications

Une exigence qui arrive souvent plus tard dans le cycle de vie d’un produit est la possibilité de fournir un historique des modifications aux entités importantes afin de répondre à la question de savoir qui l’a fait lorsqu’il a modifié les propriétés de l’entité.

L'implémenter dans une application CRUD classique est possible mais complexe.

Problème 4: Mise en œuvre de la collaboration

Les choses deviennent vraiment difficiles si vous voulez donner aux utilisateurs la possibilité de travailler en collaboration sur des objets. Dans la plupart des applications CRUD, la dernière modification écrase les modifications précédentes effectuées en parallèle par d'autres utilisateurs.

Je ne sais pas Comment souvent, je devais déjà faire face à ce problème dans ma vie. En dernier recours, la plupart des équipes implémentent une sorte de verrouillage afin d'empêcher l'édition parallèle d'objets métier. Mais pensez à un outil comme Trello – une simple solution de conseil Kanban – et combien cela serait inutile si vous ne pouviez pas collaborer avec vos collègues sur une seule carte en même temps…

Commandes à la rescousse

Pensez à la manière dont vous implémenteriez quelque chose comme Trello en utilisant uniquement des objets d'entité et CRUD. Vous pourriez dire que Trello est un exemple extrême, tout comme la collaboration, mais est-ce vraiment? Pensez à l'application (entreprise?) Sur laquelle vous travaillez actuellement et à ce que ce serait génial si elle fournissait une réelle collaboration, annulait,…

Voyons donc les commandes et comment elles peuvent nous aider à rendre notre application plus attrayante et notre code plus clair et moins sujet aux erreurs.

Déclarer des commandes

Une commande est un objet qui décrit une demande de faire quelque chose et contient exactement les informations nécessaires pour effectuer une action de un type spécifique.

Les commandes et les événements sont plus amusants dans les langages qui fournissent la syntaxe pour des classes de données sur une ligne, telles que Kotlin ou Scala. Voyons maintenant à quoi pourraient ressembler les commandes pour travailler avec des cartes sur un tableau Kanban dans Kotlin:

// Commands.kt
typealias UserId = Chaîne

Interface Command {
val user: UserId
}

// CardMessages.kt
typealias CardId = Chaîne

classe de données CardPosition (val colonne: ColumnId, val avant: CardId?)

classe scellée CardCommand: Commande {
carte abstraite: CardId
}

classe de données CreateCard (
redéfinir le nom d'utilisateur: UserId,
val board: BoardId, val pos: CardPosition,
redéfinit la carte: CardId, titre de la chaîne: String
): CardCommand ()

classe de données RenameCard (
écrasement utilisateur: UserId, écrasement carte: CardId,
val currentTitle: String, titre de val: String
): CardCommand ()

classe de données MoveCard (
écrasement utilisateur: UserId, écrasement carte: CardId,
val currentPos: CardPosition, val pos: CardPosition
): CardCommand ()

classe de données DeleteCard (
Remplacez l'utilisateur: UserId, remplacez la carte: CardId
): CardCommand ()

Ne vous souciez pas des détails de Kotlin, comme les classes scellées et les déclarations de type – je les expliquerai dans un autre article.

A partir du code ci-dessus, vous pouvez déduire certaines des caractéristiques des commandes:

  • Une commande contient toutes les informations nécessaires à l'exécution de l'opération demandée.
  • Une commande contient toutes les informations nécessaires pour valider l'opération demandée.
  • Une commande est un objet léger immuable une fois. Vous le créez quand vous en avez besoin et le déposez après.

Lorsque vous travaillez avec des commandes créées par le client, il est préférable de travailler avec la création d'identifiant décentralisé (UUID). Cela évite des problèmes tels que, par exemple, la double création d'entités.

Validation de la commande

Je ne saurais trop insister sur le fait que les commandes sont demandes faire quelque chose. Ainsi, une commande a besoin de toutes les informations nécessaires pour valider si la demande est valide et peut être traitée. C’est aussi un avantage par rapport à l’envoi d’entités: les entités ne contiennent que leurs données – les données nécessaires à la validation doivent être collectées ailleurs.

Dans le code ci-dessus, vous pouvez voir par exemple que nous avons inclus des informations sur l'utilisateur qui a soumis la commande. Cela nous permet de vérifier si l’utilisateur est autorisé à exécuter cette demande ou à le rejeter autrement.

Vous pouvez vous interroger sur la propriété currentTitle de la commande RenameCard: Cela nous permet de vérifier que le client (interface graphique, application) qui envoie la commande est synchronisé. Si les titres currentTitled ne correspondent pas au titre actuel de la carte tel qu’il est connu du serveur, il est possible que nous refusions la demande.

Commandes de traitement

Voyons maintenant comment traiter les commandes. Supposons qu’ils sont enracinés dans un récepteur de commande central qui les transmet ensuite en fonction de leur type à des récepteurs spécialisés:

// CommandReceiver.kt
...
fun receive (cmd: Command) = quand (cmd) {
est BoardCommand -> boardCommandReceiver.receive (cmd)
est ColumnCommand -> columnCommandReceiver.receive (cmd)
est CardCommand -> cardCommandReceiver.receive (cmd)
...
sinon -> Unité
}
...

Le récepteur de commande spécialisé implémente notre logique métier. Il valide la commande et la rejette si nécessaire. Si la validation réussit, l’état de notre entité mute:

// CardCommandReceiver.kt
...
fun receive (cmd: CardCommand) = quand (cmd) {
est CreateCard -> validateAndCreateCard (cmd)
est RenameCard -> validateAndRenameCard (cmd)
est MoveCard -> validateAndMoveCard (cmd)
est DeleteCard -> validateAndDeleteCard (cmd)
}

...

amusement privé validateRenameCard (cmd: RenameCard) {
val user = getUser (cmd.user) // est lancé si l'utilisateur n'existe pas
val card = getCard (cmd.card) // jette si la carte n’existe pas
si (! user.canModify (carte))
jette NotAllowedException ()
if (card.title! = card.currentTitle)
throw IllegalStateException ("Changement simultané")
cardDao.renameCard (card.id, cmd.title)
}

...

Problèmes résolus

Permet de vérifier quels problèmes mentionnés ci-dessus ont été résolus jusqu'à présent.

Nous ne traitons plus avec objets d'entité de grande taille avec beaucoup de champs optionnels. En fin de compte, nous pouvons travailler avec un objet de carte représentant notre carte pour l'afficher (plus à ce sujet dans la partie suivante). Dès que nous voulons muter notre carte, nous n’utilisons pas du tout la classe d’entités. Au lieu de cela, nous envoyons de petits objets de commande spécialisés, immuables, décrivant exactement notre intention. Pour une commande unique, notre couche service (nos destinataires de commande) sait clairement quelles propriétés de la commande peuvent être considérées comme non nulles, ce qui réduit le nombre d'exceptions NullPointerExceptions (et les élimine totalement lorsque nous utilisons Kotlin ici).

Concernant notre deuxième problème, nous n’avons pas encore mis en œuvre annuler, mais cela devient aussi simple que cela:

fun undoCommandFor (cmd: CardCommand) = quand (cmd) {
est CreateCard ->
DeleteCommand (cmd.user, cmd.card)
est RenameCard ->
RenameCard (cmd.user, cmd.card, cmd.title, cmd.currentTitle)
est MoveCard ->
MoveCard (cmd.user, cmd.card, cmd.pos, cmd.currentPos)
est-ce que DeleteCard -> ...
}

Voyez comme il est facile d’inverser la plupart des commandes pour les annuler. Simplement la commande DeleteCard n’est pas aussi simple, car vous auriez vraiment besoin de restaurer l’ensemble de l’objet …

Il serait également assez facile de mettre en place un changer l'histoire basé sur les commandes exécutées avec succès, mais cela deviendra encore plus facile en fonction des événements comme vous pourrez le voir dans le prochain post.

Même est vrai pour collaboration: Nous avons déjà fait des choses beaucoup mieux ici car les modifications simultanées ne se substitueront plus les unes aux autres. Au lieu d'écrire un objet entier lors de l'édition de son titre, nous demande notre couche de services vient de renommer le titre. Si un autre utilisateur déplace la carte au même moment, les deux modifications seront appliquées indépendamment l'une de l'autre. Si deux utilisateurs modifient le titre d’une carte, le premier gagne et la requête du deuxième utilisateur échoue avec une exception IllegalStateException, car le paramètre currentTitle de la commande ne sera plus "valide".

Toutefois, le sujet de la collaboration n’a toujours pas été résolu: nous devons trouver un moyen d’envoyer les mises à jour de statut à tous les clients. Les commandes ne sont pas parfaites ici demandes et nous devons communiquer faits aux clients à la place. Plus à ce sujet dans la partie 2…

Nouveaux problèmes créés

Les commandes ne sont pas une solution miracle et créent de nouveaux problèmes par elles-mêmes. Le plus gros défi est de trouver le niveau de granularité approprié. Il est évident que nous ne pouvons pas créer de commandes pour mettre à jour chaque propriété d’une entité.

Conclusion

Penser aux commandes est un excellent moyen de réfléchir aux cas d'utilisation que notre application doit prendre en charge.

Les commandes expriment des intentions fines de ce qu'un utilisateur veut changer dans notre système. Au lieu d’envoyer des objets d’entités complètes (ou même pire: des objets partiels avec de nombreuses propriétés NULL donnant lieu à NullPointerExceptions), nous envoyons des objets ponctuels sur mesure décrivant exactement la demande de l’utilisateur.

Nos destinataires de commandes implémentent la logique métier en validant les commandes et en les appliquant aux données conservées au cas où elles seraient valides. Les commandes contiennent toutes les informations nécessaires à la validation et à l'exécution. Ainsi, notre code devient très clair car le traitement des commandes suit toujours le même modèle.

L'implémentation de la fonctionnalité d'annulation est devenue une tâche plutôt triviale, car générer des variantes inversées de commandes est facile dans la plupart des cas. Enfin, les commandes constituent une bonne base pour la mise en œuvre d’un historique des modifications et d’une édition collaborative.

Dans la prochaine partie, nous examinerons les événements et verrons comment ils sont liés aux commandes et comment nous pouvons en tirer profit.


Commandes et événements au lieu de CRUD – Partie 1: Commandes a été publié à l'origine dans Hacker midisur Medium, où les gens poursuivent la conversation en soulignant et en répondant à cette histoire.

Source

A propos newstrotteur-fr

Découvrez également

Le chapeau noir, l’éthique et le hacker WHOIS

Tous les pirates ne sont pas mauvais. Pourtant, lorsque le terme «bidouillage» est utilisé dans …

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Do NOT follow this link or you will be banned from the site!