Architectures MVC et MVVM
Ce que ce chapitre apporte
- Expliquer pourquoi séparer présentation et logique métier.
- Décrire le rôle de chaque composant de MVC.
- Décrire le rôle de chaque composant de MVVM.
- Identifier ce que la liaison de données change dans la répartition des responsabilités.
- Choisir entre les deux selon le contexte technique.
- Reconnaître les dérives courantes de chaque modèle.
Le problème
Dans une application écrite sans découpage, une classe de formulaire contient couramment la lecture des champs, la validation des saisies, l'appel à la base de données, le calcul, la mise en forme et l'affichage du résultat.
Trois conséquences suivent.
Le métier n'est pas testable sans instancier l'interface, ce qui suppose un environnement graphique et rend les tests lents et fragiles.
Le code n'est pas réutilisable : la même règle de calcul devra être réécrite pour une version en ligne de commande ou pour une interface web.
Les modifications se propagent : changer la présentation risque de casser le calcul, puisque les deux sont entrelacés.
MVC
Le modèle porte les données et les règles métier. Il ignore complètement comment il sera affiché.
La vue présente les données à l'utilisateur. Elle ne contient aucune règle métier.
Le contrôleur reçoit les actions de l'utilisateur, décide quoi faire, sollicite le modèle et choisit la vue à afficher.
utilisateur
| action
v
Controleur ----- sollicite -----> Modele
| |
| choisit | fournit les donnees
v v
Vue <---------------------------- +
|
v
affichage
Le flux est directionnel : l'action entre par le contrôleur, traverse le modèle, ressort par la vue. C'est ce qui rend le modèle particulièrement adapté au web, où chaque requête suit exactement ce trajet.
Le contrôleur ne doit qu'orienter : recevoir, valider la forme de l'entrée, appeler le service qui convient, choisir la vue. Dès qu'il calcule, la logique redevient dépendante de l'interface, et le découpage ne sert plus à rien.
MVVM
Le modèle est identique à celui de MVC : données et règles métier.
La vue décrit l'interface, le plus souvent de façon déclarative. Elle ne contient pratiquement pas de code.
Le modèle de vue expose les données du modèle sous la forme dont la vue a besoin, ainsi que les commandes correspondant aux actions possibles.
La différence tient au lien entre la vue et le modèle de vue. Au lieu qu'un contrôleur pousse les données vers la vue, la vue se lie aux propriétés du modèle de vue. Quand une propriété change, l'affichage se met à jour sans que personne ne l'ait demandé explicitement.
Vue <====== liaison bidirectionnelle ======> ModeleDeVue -----> Modele
(declarative) (etat affichable,
commandes)
La liaison de données est le mécanisme par lequel un élément d'interface reste synchronisé avec une propriété d'objet. En liaison bidirectionnelle, une saisie de l'utilisateur met à jour la propriété, et une modification de la propriété met à jour l'affichage.
En .NET, elle repose sur l'interface INotifyPropertyChanged, que le modèle de vue implémente pour signaler ses changements.
Cette distinction est ce qui rend le modèle de vue testable. On peut vérifier qu'après telle commande le bouton devient inactif, sans jamais ouvrir de fenêtre.
Ce qui sépare les deux
| MVC | MVVM | |
|---|---|---|
| Élément intermédiaire | contrôleur | modèle de vue |
| Sens du lien avec la vue | le contrôleur choisit la vue | la vue se lie au modèle de vue |
| Liaison de données | absente ou marginale | centrale |
| Le composant intermédiaire connaît la vue | oui | non |
| Contexte typique | applications web | interfaces riches, WPF, MAUI |
Le point qui décide est le dernier de la liste des rôles : un contrôleur MVC sait quelle vue il affiche, alors qu'un modèle de vue ignore l'existence de la vue. Il expose des propriétés ; qui les observe ne le regarde pas.
Cette ignorance a une conséquence pratique importante. Le même modèle de vue peut servir à plusieurs vues, et il se teste sans interface, puisqu'il n'en connaît aucune.
Une application de bureau maintient un état affiché qui évolue en continu sous l'effet de l'utilisateur et de traitements de fond. C'est là que la liaison de données évite d'écrire des dizaines de mises à jour manuelles.
Les dérives à connaître
Chaque modèle a la sienne, et elles ont la même origine : placer du métier là où il ne va pas.
Pour MVC, le contrôleur obèse décrit plus haut. Le symptôme est un contrôleur de plusieurs centaines de lignes qui manipule directement des données.
Pour MVVM, le modèle de vue obèse, qui absorbe la logique métier parce qu'il est plus commode d'y écrire le calcul que de créer un service. Le modèle se vide, le modèle de vue devient impossible à réutiliser.
Une troisième dérive touche les deux : le modèle anémique, réduit à des propriétés sans comportement, toute la logique ayant migré ailleurs. Les objets métier ne sont plus que des sacs de données, et les règles se retrouvent dispersées.
Une quantité commandée ne doit pas dépasser le stock disponible. Cette contrainte est une règle métier : elle appartient au modèle, et doit y être vérifiée même si l'interface l'a déjà contrôlée. L'interface peut être contournée, le modèle non.
Exercices type
Où placer le calcul d'un total avec remise ?
Dans le modèle, ou dans un service métier qu'il utilise.
C'est une règle qui appartient au domaine, indépendante de la façon dont on la déclenche. Elle doit donner le même résultat qu'on passe par l'interface graphique, par une ligne de commande ou par un appel programmatique.
La placer dans le contrôleur ou le modèle de vue la rend inaccessible aux autres points d'entrée, et impose de la dupliquer le jour où l'application gagne une seconde interface.
Test rapide : si le calcul doit être vérifié par un test qui ne mentionne aucune interface, il appartient au modèle.
Quelle est la différence essentielle entre un contrôleur et un modèle de vue ?
Le contrôleur connaît la vue : il en choisit une et lui transmet les données.
Le modèle de vue ignore la vue. Il expose des propriétés et des commandes ; le mécanisme de liaison se charge du reste.
Cette différence a deux effets. Le modèle de vue se teste sans interface, puisqu'il n'en référence aucune. Et il peut servir à plusieurs vues, par exemple une version bureau et une version tactile, sans modification.
Que fait INotifyPropertyChanged ?
Elle définit un événement que l'objet déclenche quand une de ses propriétés change.
Le mécanisme de liaison s'y abonne. Quand le modèle de vue modifie une propriété et signale le changement, tous les éléments d'interface liés à cette propriété se rafraîchissent.
C'est une application directe du patron observateur du chapitre précédent : un sujet qui notifie, des abonnés qui réagissent, et un sujet qui ignore qui l'observe.
Sans cette interface, la liaison fonctionne dans le sens de la saisie vers l'objet, mais pas dans l'autre : l'affichage ne se met pas à jour quand la donnée change en arrière-plan.
Pourquoi MVC est-il dominant sur le web et MVVM sur le bureau ?
À cause de la nature de l'état.
Une requête web est sans état du point de vue du serveur : elle arrive, elle est traitée, une réponse est produite, tout est oublié. Le flux directionnel de MVC épouse ce cycle. Une liaison de données n'aurait rien à maintenir dans le temps.
Une application de bureau conserve un état affiché qui évolue en continu. Sans liaison, chaque changement obligerait à écrire manuellement la mise à jour de chaque élément concerné, ce qui devient rapidement ingérable.
Les frameworks web modernes qui maintiennent un état côté navigateur adoptent d'ailleurs des mécanismes proches de la liaison de données, ce qui confirme que le critère est bien l'état et non la plateforme.
Un modèle réduit à des propriétés sans méthode : quel problème ?
C'est le modèle anémique. Les objets métier deviennent des structures de données, et les règles qui les concernent se retrouvent dispersées dans les services, les contrôleurs et les modèles de vue.
Deux effets suivent. La même règle finit dupliquée à plusieurs endroits, avec des variantes. Et rien ne garantit qu'un objet est dans un état valide, puisqu'aucune méthode ne contrôle ses transitions.
Le remède consiste à ramener dans l'objet les règles qui portent sur son propre état. Une commande doit savoir refuser une ligne négative ; ce n'est pas au contrôleur de le vérifier avant chaque appel.
Faut-il valider deux fois, dans l'interface et dans le modèle ?
Oui, mais pas la même chose.
L'interface valide la forme pour donner un retour immédiat : champ obligatoire rempli, format de date correct, nombre bien entier. Ce contrôle sert le confort d'usage.
Le modèle valide les règles métier, et il le fait sans supposer que l'interface a déjà contrôlé quoi que ce soit. Une interface peut être contournée, un service peut être appelé directement, une version future peut oublier un contrôle.
La duplication n'en est pas vraiment une, puisque les deux contrôles n'ont ni le même objet ni la même finalité. Ce qui serait une duplication fautive, c'est de réécrire la règle métier dans l'interface.
La méthode
- Demande-toi si la règle changerait avec l'interface. Si non, elle appartient au modèle.
- Garde le contrôleur mince : recevoir, valider la forme, appeler, choisir la vue.
- Vérifie que le modèle de vue ne référence aucune vue. S'il en connaît une, le découpage est perdu.
- Implémente
INotifyPropertyChangeddès que la vue doit refléter un changement venu d'ailleurs. - Laisse les règles dans les objets métier plutôt que de les extraire toutes dans des services.
- Valide la forme côté interface, les règles côté modèle, sans supposer l'un depuis l'autre.
En résumé
- Les deux modèles visent à rendre la logique métier indépendante de la présentation.
- MVC : le contrôleur reçoit l'action, sollicite le modèle, choisit la vue. Flux directionnel, adapté au web.
- MVVM : la vue se lie aux propriétés du modèle de vue, qui ignore l'existence de la vue.
- La liaison de données repose sur une notification de changement, c'est-à-dire sur le patron observateur.
- Le modèle de vue porte l'état de l'affichage, pas les règles métier.
- Dérives : contrôleur obèse, modèle de vue obèse, modèle anémique. Toutes déplacent le métier hors du modèle.
- Le critère de choix est l'état : sans état par requête, MVC ; état affiché persistant, MVVM.
Et ensuite ? Le logiciel conçu et structuré s'exécute sur un système qui lui alloue un processeur et de la mémoire. Les chapitres suivants descendent d'un niveau, en commençant par les processus et leur ordonnancement.