web-dev-qa-db-fra.com

Couche de présentation accédant à la logique métier

J'ai lu récemment beaucoup de documents sur DDD (objets d'entité commerciale) et d'autres modèles courants dans l'architecture à plusieurs niveaux (en couches). Une chose qui me pose problème, c'est que la plupart des articles, blogs, exemples, etc. semblent ne parler que d'un aspect d'un système. Certains peuvent parler de la création d'un BLL pour contenir la logique métier. Certains ne parlent que de sauvegarde données via un DAL (ignorant la lecture). Certains ne parlent que des DTO. Je n'ai encore rien trouvé qui, au niveau de base, parle de mettre tout cela ensemble. Lorsque j'essaie de rassembler une grande partie de ce matériel moi-même, il semble que les informations soient parfois mutuellement exclusives.

Voici où (pour moi) les choses ont tendance à s'exclure mutuellement:

  • La logique métier est contenue dans les objets d'entité métier DDD, que votre interface utilisateur ne crée pas directement (elle obtient des DTO).
  • Les données sont transmises à l'interface utilisateur via les DTO, qui doivent pas contenir la logique métier. Le DTO ne doit contenir que des getters et setters.

Ce qui m'amène à Question:

Comment votre interface utilisateur se configure-t-elle lorsque la logique métier est requise pour analyser les données de ces DTO afin de prendre des décisions concernant l'interface utilisateur?

Par exemple, vous développez l'interface utilisateur et vous avez cette exigence. Vous devez déterminer dans le code si le bouton Supprimer doit être activé ou non:

Configuration requise: une commande ne peut être supprimée que si aucun produit ne figure sur la commande et que le statut de la commande est "Ouvert".

Bien sûr, vous pouvez vérifier votre interface utilisateur avec les éléments suivants:

Dim _order As OrderDTO = SomeServiceCall.ReadOrder(123)

If _order.Products.Count = 0 And _order.Status = "Open" Then 
    DeleteButton.Enabled = True
End If

Mais cela ne met-il pas maintenant la logique métier dans le code de l'interface utilisateur? Que se passe-t-il s'il existe des règles commerciales nouvelles ou modifiées pour déterminer quand une commande peut être supprimée? Alors que vous pourriez ajouter une fonction .CanDelete() au OrderDTO, ce n'est pas le bon endroit en fonction de tout ce que j'ai lu, car la logique métier appartient à l'objet d'entité Order. Alors, comment dites-vous à l'interface utilisateur d'activer ou de désactiver ce sacré bouton Supprimer ???

Une option peut être de toujours activer le bouton Supprimer et de lever une exception lorsque l'action Supprimer échoue à la validation dans l'objet d'entité Order (note latérale: vient de trouver FluentValidation et cela semble incroyable!). Mais c'est une interface utilisateur bâclée. Quelles sont les alternatives de "bonne architecture" pour obtenir l'interface utilisateur dont elle a besoin?

La même chose pourrait s'appliquer à de nombreuses autres situations d'interface utilisateur, où, sur la base de différentes combinaisons de données (c'est-à-dire la logique métier) contenues dans un DTO, vous pouvez verrouiller certains champs de saisie, ou les masquer, etc.

Souvent, il semble que les articles sur DDD, DAL, BLL, DTO ignorent les exigences pratiques de l'interface utilisateur.

9
HardCode

Configuration requise: l'interface utilisateur doit uniquement activer le bouton Supprimer pour une commande lorsqu'il n'y a aucun produit sur la commande et que le statut de la commande est "Ouvert".

C'est l'erreur classique de laisser les détails de l'implémentation s'infiltrer dans une exigence. Au point, cette exigence n'a absolument aucun droit de savoir que la façon dont un utilisateur demande une suppression est en utilisant un bouton. L'exigence n'est pas du tout une exigence d'interface utilisateur.

La question la plus fondamentale dans la conception de logiciels est: "Qu'est-ce qui sait quoi?"

C'est le travail de l'interface utilisateur de savoir qu'il y a un bouton. Ce n'est pas le travail des couches de présentation. Le travail des couches de présentation consiste à savoir qu'une fonction de commande est désactivée. Ne sait pas si c'est un bouton. Ne sait pas s'il est étiqueté delete, effacer ou löschen.

Isoler la connaissance c'est le point de tout cela. Donc oui. Ce code ne devrait pas être dans l'interface utilisateur.

Mais .CanDelete() est une stratégie cassée simplement parce qu'elle ne présente pas l'état activé/désactivé à l'utilisateur avant d'émettre la commande (quelle que soit la manière dont il la lance). C'est votre exigence d'interface utilisateur.

L'activation et la désactivation dans l'interface utilisateur ne nécessitent pas de logique métier dans l'interface utilisateur. Il nécessite uniquement que l'interface utilisateur accepte les mises à jour de statut chaque fois qu'elles sont envoyées. Il n'a pas besoin de comprendre pourquoi.

Donc, à moins que vous ne souhaitiez interroger en bouclant à l'infini sur .CanDelete() ce que vous devriez faire est d'appeler cette logique métier chaque fois que _order.Products.Count ou _order.Status aurait pu changer d'état.

Vous voulez mettre tout cela ensemble? Commencez avec quelque chose qui fonctionne. Ensuite, repérez les endroits où trop d'idées ont été mélangées et taquinez-les. Faites-le suffisamment de fois et vous commencerez à voir ce qu'il faut séparer au début et économisez du temps.

Il est très important de commencer de cette façon. Si vous ne construisez que des systèmes dont les préoccupations sont parfaitement séparées, vous n'apprendrez jamais à réparer les systèmes qui les mélangent.

5
candied_orange

Que se passe-t-il s'il existe des règles commerciales nouvelles ou modifiées pour déterminer quand une commande peut être supprimée? Bien que vous puissiez ajouter une fonction .CanDelete () à OrderDTO, ce n'est pas le bon endroit en fonction de tout ce que j'ai lu, car la logique métier appartient à l'objet d'entité Order.

Ce qui fonctionne pour moi: je considère le modèle de domaine comme une machine à états finis. La machine d'état réagit aux messages , où les messages peuvent être des choses simples comme un "temps écoulé" avec une lecture à partir d'un horodatage d'une horloge locale, ou quelque chose de compliqué comme un agent de réservation sélectionnant un itinéraire spécifique pour le fret. La machine d'état copie les informations souhaitées à partir du message et calcule son état suivant (qui pourrait même être l'état actuel, s'il n'y a pas de transition ou si la transition revient à l'état actuel).

Lorsque le modèle décrit son état actuel (c'est-à-dire fournissant une vue pour le cas d'utilisation du changement), il le fait non seulement en offrant une représentation de son état calculé, mais aussi une représentation des commandes candidates qu'il est prêt à recevoir.

Dans votre exemple spécifique, cette liste de commandes candidates pour une commande vide comprendrait à la fois la commande "ajouter un élément" et la commande "supprimer la commande", mais dans le cas où la logique du domaine interdit la suppression, la commande de suppression n'est pas présente dans la liste des candidats.

C'est alors, comme l'a souligné @candied_orange, le travail de quelqu'un d'autre de comprendre comment traduire chacune des commandes candidates dans les opportunités utilisateur appropriées.

Par exemple, dans une interface Web, vous pouvez avoir un module qui prend la liste des commandes candidates et crée des formulaires HTML pour chacune. Ce n'est pas de la "logique de domaine", dans le sens où ce n'est pas prendre les décisions du modèle de domaine, mais cela se traduit du message des modèles de domaine en une représentation plus générale.

Mais vous pouvez tout aussi facilement remplacer l'interface Web par une interface de ligne de commande; la liste des commandes candidates est inchangée, mais leur expression dans l'interface de ligne de commande est certainement différente.

Dans un sens, l'ajout de ".CanDelete" au DTO est exactement le bon type d'idée, bien qu'il puisse ne pas utiliser cette orthographe. Votre DTO est une structure de données immuable avec une représentation en mémoire de la liste des commandes candidates, et vous pouvez choisir la conception que vous souhaitez pour l'interroger.

Dim _order As OrderDTO = SomeServiceCall.ReadOrder(123)

If _order.commands.contains("http://example.org/commands/deleteOrder") Then 
    DeleteButton.Enabled = True
End If

Rappelez-vous, l'idée de base d'un Data Transfer Object est qu'il est conçu pour réduire le nombre d'appels de méthode, c'est-à-dire qu'il contient de nombreuses réponses intéressantes. Nous encodons donc des informations redondantes dans le DTO, et en retour, nous obtenons une logique de domaine centralizd qui est plus facile à modifier.

Souvent, il semble que les articles sur DDD, DAL, BLL, DTO ignorent les exigences pratiques de l'interface utilisateur.

Oui. DDD en particulier est mauvais pour discuter de la "plomberie" et des vraies complications qu'elle peut entraîner.

3
VoiceOfUnreason

Je ressens votre douleur, et j'ai fait les mêmes observations il y a quelques années, ce qui m'a conduit dans le terrier du lapin à réévaluer quelle orientation d'objet est censée être et ce qui constitue une conception maintenable. Réponse courte: Vous ne pouvez pas trouver beaucoup de choses dans ce sujet, car cela ne fonctionne pas . Les architectures à N niveaux, le DDD (tel que pratiqué par la plupart) et les DTO en particulier sont tous des idées sous-optimales. La couche métier indépendante de la persistance ne fonctionne pas. L'interface utilisateur indépendante de ces conceptions n'existe pas vraiment.

Premièrement: il est tout à fait raisonnable de devoir griser certains boutons en fonction de certaines règles. L'interface utilisateur est ce que les utilisateurs voient, il est tout à fait naturel pour eux d'adopter la terminologie de l'interface utilisateur.

Comment mettre en œuvre: Réfléchissons à cette étape à la fois. Où est supposée la connaissance pour déterminer si un Order est supprimable? Il semble assez raisonnable de s'attendre à cela dans le Order lui-même. Cela dépend de l'état interne et de la sémantique du Order, donc il devrait être là.

Alors, comment les informations "parviennent-elles à l'interface utilisateur" à partir du Order? C'est là que tous les modèles ci-dessus échouent. Tous essaient de récupérer ces informations du Order d'une manière ou d'une autre. Et quelle que soit la façon dont vous le faites: qu'il s'agisse d'un nouveau champ dans le DTO, qu'il s'agisse de mises à jour de statut ou d'événements, ou quoi que ce soit, cela signifie toujours que maintenant vous couple toutes ces choses à cette fonctionnalité simple.

La seule façon d'obtenir ceci en un seul endroit et de le maintenir est de ne pas extraire ces informations du Order du tout. Au lieu de cela, obtenez le comportement qui a besoin de ces informations dans le Order, c'est-à-dire présenter le Order sur l'interface utilisateur.

Il s'ensuit donc assez raisonnablement que le Order devrait pouvoir se présenter, avec le bouton de suppression désactivé/activé et tout. En effet, l'interface utilisateur ne devrait pas dépendre de l '"entreprise", "l'entreprise" ne devrait pas dépendre de l'interface utilisateur. (Ou le "Business" a une interface utilisateur si vous voulez)

J'espère que cela vous semble tout à fait raisonnable également, même si cela va à l'encontre de presque tout ce que nous ont dit ces modèles d'architecture et de conception au cours des deux ou trois dernières décennies.

Exemple de mise à jour du pseudocode:

class Order {
   ...state: i.e. products, whatever...

   void cancel() { ... }

   ...

   UIComponent display() {
      return Panel(
         new Table(products),
         new Button("Track", ...),
         new Button("Cancel", products.empty?ENABLED:DISABLED, this->cancel()),
         ...);
   }
}

Cela démontre ce que je veux dire. Le Order sait se présenter. Cela suit KISS, Object-Orientation, Law of Demeter, is Maintainable, etc. Si les "Business" et "UI" sont dans la même application, il n'y a aucune raison de pas faites-le de cette façon.

Notez que la décision d'annuler la commande reste dans le Order. Notez également qu'il n'y a pas de détails sur la présentation qui fuit dans le Order lui-même. Pas de couleurs, de disposition ou de choses comme ça.

1
Robert Bräutigam

Je ne sais pas quel type d'architecture vous aviez en tête avec l'idée d'utiliser les DTO de cette façon, mais prenons le très populaire de Bob Martin Clean architecture et voyons comment le problème est résolu là-bas.

Dans une telle architecture, un objet UI fournirait une interface pour

  • obtenir des données de commande et pour

  • boutons d'activation et de désactivation (mais sans aucune logique comment et quand le faire).

Un objet Presenter (qui contient une telle instance d'interface) pourrait récupérer les données de l'entité de commande, transmettre ces données - peut-être en tant que DTO de commande - à l'interface utilisateur via l'interface, vérifier la méthode .CanDelete() de la commande et appelez l'interface pour désactiver ou activer le bouton Supprimer en conséquence. Le présentateur peut également écouter certains événements du système qui peuvent changer l'état de la commande et répéter ensuite l'évaluation de .CanDelete() (et la mise à jour de l'interface utilisateur).

Donc, en bref, je ne sais pas à quels "articles, blogs, exemples, etc." vous faites référence, mais l'ingrédient qui semble manquer, ce sont simplement des interfaces et des événements. L'idée "Les données sont transmises à l'interface utilisateur via DTO" peut être simplement un malentendu ou une simplification excessive, mais puisque vous n'avez donné aucune référence, je ne peux pas vraiment vous dire quelle est la source d'où vous avez obtenu cela.

1
Doc Brown

Étant donné que le DTO n'a pas besoin d'être 1 à 1 avec les entités de données, j'irais avec votre CanDelete () dans le DTO en utilisant les règles métier du BL. Je le reconditionnerais probablement en tant qu'attribut "IsDeletable" pour le rendre plus semblable aux données.

S'il s'agit d'un modèle régulier, vous souhaiterez peut-être incorporer une structure quelconque. (IsDeletable, IsUpdatable).

Si vous faisiez une API au lieu d'une interface utilisateur (et une API est un type d'interface utilisateur à mon avis), c'est un peu comme https://en.wikipedia.org/wiki/HATEOAS en résumé.

Il y a beaucoup de dogmes ainsi qu'une pratique parfaitement valable qui n'est pas en faveur. Choisir ce qui fonctionne pour vous et vous permet de livrer quelque chose tout en trouvant l'équilibre entre le serrage à la main perfectionniste et le piratage est une compétence.

1
LoztInSpace