MVVM et modèle de service

14

Je crée une application WPF en utilisant le modèle MVVM. À l'heure actuelle, mes viewmodels appellent la couche de service pour récupérer des modèles (ce qui n'est pas pertinent pour le viewmodel) et les convertir en viewmodels. J'utilise l'injection de constructeur pour transmettre le service requis au viewmodel.

Il est facilement testable et fonctionne bien pour les modèles de vue avec peu de dépendances, mais dès que j'essaie de créer des modèles de vue pour des modèles complexes, j'ai un constructeur avec beaucoup de services injectés (un pour récupérer chaque dépendance et une liste de toutes les valeurs disponibles pour se lier à un itemsSource par exemple). Je me demande comment gérer plusieurs services comme ça et avoir toujours un modèle de vue que je peux tester facilement.

Je pense à quelques solutions:

  1. Création d'un singleton de services (IServices) contenant tous les services disponibles en tant qu'interfaces. Exemple: Services.Current.XXXService.Retrieve (), Services.Current.YYYService.Retrieve (). De cette façon, je n'ai pas un énorme constructeur avec une tonne de paramètres de services.

  2. Création d'une façade pour les services utilisés par le viewModel et passage de cet objet dans le ctor de mon viewmodel. Mais alors, je vais devoir créer une façade pour chacun de mes modèles de vues complexes, et ça pourrait être un peu trop ...

Selon vous, quelle est la "bonne" façon de mettre en œuvre ce type d'architecture?

alfa-alfa
la source
Je pense que la "bonne" façon de le faire est de créer une couche distincte qui appelle les services et fait le casting nécessaire pour créer le ViewModel. Vos ViewModels ne devraient pas être responsables de leur création.
Amy Blankenship
@AmyBlankenship: les modèles de vue ne devraient pas avoir (ou même nécessairement pouvoir) se créer eux-mêmes, mais seront inévitablement parfois responsables de la création d' autres modèles de vue . Un conteneur IoC avec prise en charge d'usine automatique est d'une grande aide ici.
Aaronaught
"Will parfois" et "devrait" sont deux animaux différents;)
Amy Blankenship
@AmyBlankenship: Suggérez-vous que les modèles de vue ne devraient pas créer d'autres modèles de vue? C'est une pilule difficile à avaler. Je peux comprendre que les modèles de vue ne devraient pas être utilisés newpour créer d'autres modèles de vue, mais pensez à quelque chose d'aussi simple qu'une application MDI où cliquer sur un bouton ou un menu "nouveau document" ajoutera un nouvel onglet ou ouvrira une nouvelle fenêtre. L'enveloppe / le conducteur doit être capable de créer de nouvelles instances de quelque chose , même s'il est caché derrière une ou quelques couches d'indirection.
Aaronaught
Eh bien, il doit certainement avoir la capacité de demander qu'une vue soit faite quelque part. Mais pour le faire lui-même? Pas dans mon monde :). Mais là encore, dans le monde dans lequel je vis, nous appelons VM «modèle de présentation».
Amy Blankenship

Réponses:

22

En fait, ces deux solutions sont mauvaises.

Création d'un singleton de services (IServices) contenant tous les services disponibles en tant qu'interfaces. Exemple: Services.Current.XXXService.Retrieve (), Services.Current.YYYService.Retrieve (). De cette façon, je n'ai pas un énorme constructeur avec une tonne de paramètres de services.

Il s'agit essentiellement du modèle de localisation de service , qui est un anti-modèle. Si vous faites cela, vous ne pourrez plus comprendre de quoi dépend réellement le modèle de vue sans regarder son implémentation privée, ce qui rendra très difficile le test ou la refactorisation.

Création d'une façade pour les services utilisés par le viewModel et passage de cet objet dans le ctor de mon viewmodel. Mais alors, je vais devoir créer une façade pour chacun de mes modèles de vues complexes, et ça pourrait être un peu trop ...

Ce n'est pas tellement un anti-modèle mais c'est une odeur de code. Essentiellement, vous créez un objet paramètre , mais le motif du modèle de refactorisation PO est de traiter des ensembles de paramètres qui sont utilisés fréquemment et dans de nombreux endroits différents , alors que ce paramètre ne sera utilisé qu'une seule fois. Comme vous le mentionnez, cela créerait beaucoup de gonflement de code sans aucun avantage réel et ne serait pas agréable avec beaucoup de conteneurs IoC.

En fait, les deux stratégies ci-dessus négligent le problème global, à savoir que le couplage est trop élevé entre les modèles de vue et les services . Il suffit de cacher ces dépendances dans un localisateur de service ou d'un objet de paramètre ne fait pas changer combien d'autres objets du modèle de vue dépend.

Pensez à la façon dont vous testeriez à l'unité l'un de ces modèles de vue. Quelle sera la taille de votre code d'installation? Combien de choses doivent être initialisées pour que cela fonctionne?

Beaucoup de gens qui commencent avec MVVM essaient de créer des modèles de vue pour un écran entier , ce qui est fondamentalement la mauvaise approche. MVVM est tout au sujet de la composition , et un écran avec de nombreuses fonctions doit être composé de plusieurs modèles de vue différents, chacun dépendant d'un seul ou de quelques modèles / services internes. S'ils ont besoin de communiquer entre eux, vous le faites via pub / sub (courtier de messages, bus d'événements, etc.)

Ce que vous devez réellement faire est de refactoriser vos modèles de vue afin qu'ils aient moins de dépendances . Ensuite, si vous avez besoin d'un "écran" agrégé, vous créez un autre modèle de vue pour agréger les modèles de vue plus petits. Ce modèle de vue agrégée n'a pas grand-chose à lui seul, il est donc également assez facile à comprendre et à tester.

Si vous l'avez fait correctement, cela devrait être évident simplement en regardant le code, car vous aurez des modèles de vue courts, succincts, spécifiques et testables.

Aaronaught
la source
Ouais, c'est ce que je vais probablement finir par faire! Merci beaucoup monsieur.
alfa-alfa
Eh bien, je pensais déjà qu'il avait déjà essayé ça, mais sans succès. @ alfa-alfa
Euphoric
@Euphoric: Comment ne "réussissez-vous" pas? Comme dirait Yoda: Faites ou ne faites pas, il n'y a pas d'essais.
Aaronaught
@Aaronaught Par exemple, il a vraiment besoin de toutes les données dans un seul modèle de vue. Peut-être qu'il a une grille et que différentes colonnes proviennent de différents services. Vous ne pouvez pas faire ça avec la composition.
Euphoric
@Euphoric: En fait, vous pouvez résoudre ce problème avec la composition, mais cela peut être fait sous le niveau du modèle de vue. Il s'agit simplement de créer les bonnes abstractions. Dans ce cas, vous n'avez besoin que d'un seul service pour gérer la requête initiale afin d'obtenir une liste d'ID et une séquence / liste / tableau d '"enrichisseurs" qui annotent avec leurs propres informations. Faites de la grille elle-même son propre modèle de vue, et vous avez résolu le problème avec deux dépendances efficaces, et c'est extrêmement facile à tester.
Aaronaught
1

Je pourrais écrire un livre à ce sujet ... en fait je le suis;)

Tout d'abord, il n'y a pas de manière universellement «correcte» de faire les choses. Vous devez prendre en compte d'autres facteurs.

Il est possible que vos services soient trop fins. Envelopper les services avec des façades qui fournissent l'interface qu'un Viewmodel spécifique ou même un cluster de ViewModels associés utiliserait pourrait être une meilleure solution.

Il serait encore plus simple de regrouper les services dans une seule façade que tous les modèles de vue utilisent. Bien sûr, cela peut potentiellement être une très grande interface avec beaucoup de fonctions inutiles pour le scénario moyen. Mais je dirais que ce n'est pas différent d'un routeur de messages qui gère tous les messages de votre système.

En fait, ce que j'ai vu évoluer beaucoup d'architectures est un bus de messages construit autour de quelque chose comme le modèle Event Aggregator. Les tests y sont faciles car votre classe de test enregistre simplement un écouteur avec l'EA et déclenche l'événement approprié en réponse. Mais c'est un scénario avancé qui prend du temps à se développer. Je dis commencer par la façade unificatrice et partir de là.

Michael Brown
la source
Une façade de service massive est très différente d'un courtier de messages. C'est presque à l'extrémité opposée du spectre de dépendance. Une caractéristique de cette architecture est que l'expéditeur ne sait rien du récepteur et qu'il peut y avoir de nombreux récepteurs (pub / sub ou multicast). Peut-être que vous le confondez avec le "remoting" de style RPC qui expose simplement un service traditionnel sur un protocole distant, et l'expéditeur est toujours couplé à la réception, à la fois physiquement (adresse du point de terminaison) et logiquement (valeur de retour).
Aaronaught
La similitude est que la façade agit comme un routeur, l'appelant ne sait pas quel service / services gère l'appel tout comme un client qui envoie un message ne sait pas qui gère le message.
Michael Brown
Oui, mais la façade est alors un objet divin . Il possède toutes les dépendances des modèles d'affichage, probablement plus car il va être partagé par plusieurs. En effet, vous avez éliminé les avantages du couplage lâche pour lequel vous avez travaillé si dur en premier lieu, car maintenant, chaque fois que quelque chose touche la méga-façade, vous n'avez aucune idée de la fonctionnalité dont il dépend vraiment. Image écrivant un test unitaire pour une classe utilisant la façade. Vous créez une maquette pour la façade. Maintenant, de quelles méthodes vous moquez-vous? À quoi ressemble votre code de configuration?
Aaronaught
Ceci est très différent d'un courtier de messages car le courtier ne sait rien non plus sur l'implémentation des gestionnaires de messages . Il utilise l'IoC sous le capot. La façade sait tout sur les destinataires car elle doit leur renvoyer les appels. Le bus a un couplage nul; la façade a un couplage efférent obscénément élevé. Presque tout ce que vous changez, n'importe où, affectera également la façade.
Aaronaught
Je pense qu'une partie de la confusion ici - et je le vois beaucoup - est juste ce que signifie une dépendance . Si vous avez une classe qui dépend d'une autre classe, mais qui appelle 4 méthodes de cette classe, elle a 4 dépendances, pas 1. Mettre tout derrière une façade ne change pas le nombre de dépendances, cela les rend simplement plus difficiles à comprendre .
Aaronaught
0

Pourquoi ne pas combiner les deux?

Créez une façade et mettez tous les services que vos modèles de vue utilisent. Ensuite, vous pouvez avoir une seule façade pour tous vos modèles de vue sans le mauvais mot S.

Ou vous pouvez utiliser l'injection de propriété au lieu de l'injection de constructeur. Mais alors, vous devez vous assurer que ceux-ci sont injectés correctement.

Euphorique
la source
Ce serait une meilleure réponse si vous fournissiez un exemple en pseudo-C #.
Robert Harvey