Étude de Cas : L'Architecture MVC expliquée simplement
Pourquoi séparer notre code en plusieurs morceaux au lieu de tout mettre dans le même fichier ? Pour un étudiant, cela peut sembler être une perte de temps. Pourtant, c'est ce qui sépare un "bricolage" d'un véritable "logiciel professionnel".
1. L'analogie du Restaurant (Comprendre le MVC)
Pour comprendre le MVC (Modèle-Vue-Contrôleur), imaginez que vous allez au restaurant :
- La Vue (Le Serveur) : C'est la personne à qui vous parlez. Il vous présente le menu (interface), prend votre commande et vous apporte l'assiette. Il ne sait pas cuisiner, il fait juste le lien entre vous et la cuisine.
- Le Contrôleur (Le Chef de cuisine) : Il reçoit la commande du serveur. Il vérifie si les ingrédients sont disponibles. Il donne les ordres aux cuisiniers. C'est lui qui décide comment le travail est fait.
- Le Modèle (La Recette et les Ingrédients) : C'est la base. Une recette de gâteau ne sait pas qui va le manger, ni si le serveur est gentil. Elle définit juste ce qu'est un gâteau (farine, sucre, oeufs).
Résumé des rôles
| Concept | Dans notre projet | Son rôle résumé |
|---|---|---|
| Modèle | Client, Facture |
"Je suis une donnée brute et je vérifie que je suis valide." |
| Contrôleur | FactureController |
"Je suis le cerveau. Je reçois les ordres de l'interface et je manipule les données." |
| Vue | Console, WinForms |
"Je suis le visage de l'appli. Je montre les boutons et je capture les erreurs." |
2. Pourquoi plusieurs projets dans Visual Studio ?
Dans l'Explorateur de solutions, vous voyez trois dossiers distincts. Imaginez que ce sont des boîtes à outils différentes.
Structure physique du logiciel
graph TD
subgraph "Vues (Habillage)"
UI_Console[MvcExemple.Console]
UI_WinForms[MvcExemple.WinForms]
end
subgraph "Cerveau (Logique Partagée)"
Lib[MvcExemple.Lib]
Ctrl[Contrôleurs]
Mod[Modèles]
end
subgraph "Tests Automatisés"
Tests[MvcExemple.Tests]
end
UI_Console --> Lib
UI_WinForms --> Lib
Tests --> Lib
L'avantage concret
Si demain votre patron veut une application Mobile au lieu d'une application Windows, vous gardez la boîte Lib (le cerveau) et vous changez juste l'habillage. Vous n'avez pas à réécrire le calcul des taxes ou la validation des noms !
3. La protection des données (Le "ReadOnly")
C'est souvent le concept le plus dur à saisir : pourquoi utiliser IReadOnlyList au lieu d'une simple List ?
Analogie : Le contrôleur est comme un compte bancaire.
Le contrôleur a sa liste privée (son coffre-fort).
S'il donnait la List directe à la fenêtre (Vue), la fenêtre pourrait vider le coffre sans demander la permission !
* En donnant une IReadOnlyList, le contrôleur dit : "Voici une photo de mes données. Tu peux les regarder, mais tu ne peux rien modifier."
// Dans le contrôleur :
private List<Facture> _historique; // Le coffre-fort (privé)
public IReadOnlyList<Facture> HistoriqueReadOnly {
get { return _historique.AsReadOnly(); } // La photo (public)
}
4. Le Workflow : Voyage d'une donnée
Comprendre le MVC, c'est comprendre comment les objets communiquent entre eux. Voici ce qui se passe quand vous cliquez sur "Ajouter un item" :
Diagrammes de Séquence (Voyage d'une donnée)
Pour mieux comprendre, décomposons le flux de l'application en deux moments clés.
A. L'Initialisation (Chargement des données)
Avant de commencer, l'interface doit se remplir. La Vue demande les données aux contrôleurs spécialisés.
sequenceDiagram
participant V as Vue (UI)
participant CC as ClientController
participant IC as ItemController
V->>CC: Demande la liste des clients
CC-->>V: ListeClientsReadOnly
V->>IC: Demande le catalogue de produits
IC-->>V: CatalogueReadOnly
Note over V: Les listes (ComboBox) sont remplies
B. Le Cycle de Vente (Action de l'utilisateur)
Une fois le client sélectionné, voici comment les objets collaborent pour créer une facture.
sequenceDiagram
participant V as Vue (UI)
participant FC as FactureController
participant M as Facture (Modèle)
participant L as LigneFacture (Snapshot)
Note left of V: 1. L'utilisateur clique sur "Démarrer"
V->>FC: CreerNouvelleFacture(clientSelectionne)
FC->>M: <<create>> new(clientSelectionne)
M-->>FC:
FC-->>V:
Note left of V: 2. L'utilisateur clique sur "Ajouter"
V->>FC: AjouterItem(itemChoisi)
Note right of FC: Vérification de l'état (if != null)
FC->>M: AjouterItem(itemChoisi)
M->>L: <<create>> new(itemChoisi)
Note right of L: Copie des valeurs (Snapshot)
L-->>M:
M->>M: Ajout à la liste interne
M-->>FC:
FC-->>V:
Note left of V: 3. Mise à jour de l'affichage
V->>FC: ObtenirTotalEnCours()
FC->>M: CalculerTotal()
M-->>FC: total (decimal)
FC-->>V: total (decimal)
Analyse du Workflow avec le Code
Voyons comment ce diagramme se traduit par des lignes de code concrètes :
1. La Préparation : Obtenir les données :
Au chargement de la fenêtre, la Vue demande au contrôleur des clients les données pour remplir la liste déroulante (ComboBox).
// Vue (WinForms)
cmbClients.DataSource = _clientCtrl.ListeClientsReadOnly;
cmbClients.DisplayMember = "Nom";
2. L'Ouverture : Démarrer une facture :
Lorsque l'utilisateur clique sur "Démarrer", on récupère le client sélectionné dans la liste et on l'envoie au contrôleur de facture.
// Vue (WinForms)
Client? client = cmbClients.SelectedItem as Client; // Sélection de l'objet
if (client != null) {
_factureCtrl.CreerNouvelleFacture(client); // Ouverture de la session
}
Facture en mémoire :// Contrôleur
public void CreerNouvelleFacture(Client client) {
_factureEnCours = new Facture(client); // L'état change : session ouverte !
}
3. L'Action : Ajouter un item :
Une fois la facture ouverte, on peut y ajouter des items. Notez qu'on ne manipule pas la liste des items de la facture directement depuis la Vue. On demande au chef (le contrôleur) de le faire.
3. La Validation (Le Contrôleur veille) :
Le contrôleur vérifie si l'action est logique (est-ce qu'une facture est bien ouverte ?) avant de parler au modèle.
// Contrôleur
public void AjouterItem(Item item) {
if (_factureEnCours == null)
throw new InvalidOperationException("Pas de facture ouverte !");
_factureEnCours.AjouterItem(item);
}
4. Le Modèle crée un Snapshot (Figer le temps) :
Pourquoi copier le prix dans LigneFacture au lieu de garder l'objet Item ?
Si vous achetez un chocolat à 2$ aujourd'hui, et que demain le magasin monte le prix à 5$, votre facture d'hier ne doit pas changer ! On fait une copie de la valeur pour figer le prix au moment de la vente.
// Modèle (Facture.cs)
public void AjouterItem(Item item) {
_lignes.Add(new LigneFacture(item)); // On crée une NOUVELLE ligne
}
// Modèle (LigneFacture.cs)
public LigneFacture(Item item) {
this.Description = item.Description; // Copie du texte
this.PrixUnitaire = item.Prix; // Copie du prix ACTUEL
}
5. La gestion des erreurs (Try-Catch)
Une application professionnelle ne doit jamais planter (fermer brusquement).
- Le Modèle détecte l'erreur (ex: prix négatif) et lance un cri d'alerte :
throw new ArgumentException(). - La Vue (l'interface) doit être prête à entendre ce cri. Elle utilise un filet de sécurité : le
try-catch.
try {
_monControleur.AjouterItem(item);
}
catch (Exception ex) {
// On attrape le cri d'alerte et on l'affiche proprement au lieu de planter
MessageBox.Show(ex.Message);
}
6. Diagrammes de Classes
La Vue Globale (Indigeste)
Voici l'ensemble des relations du projet. On voit rapidement que vouloir tout afficher d'un coup devient vite illisible (le "bordel"). C'est pourtant une architecture simple !
classDiagram
class FactureController {
-Facture _factureEnCours
-List~Facture~ _historiqueFactures
+CreerNouvelleFacture(Client)
+AjouterItem(Item)
+TerminerFacture()
+HistoriqueReadOnly : IReadOnlyList~Facture~
}
class ClientController {
-List~Client~ _clients
+AjouterClient(int, string)
+ObtenirClientParId(int) : Client
+ClientsReadOnly : IReadOnlyList~Client~
}
class ItemController {
-List~Item~ _itemsDisponibles
+AjouterItemDisponible(string, decimal)
+CatalogueReadOnly : IReadOnlyList~Item~
}
class Facture {
-Client _client
-List~LigneFacture~ _lignes
+CalculerTotal() : decimal
+LignesReadOnly : IReadOnlyList~LigneFacture~
}
class LigneFacture {
-string _description
-decimal _prixUnitaire
}
class Client {
-int _id
-string _nom
}
class Item {
-string _description
-decimal _prix
}
FactureController "1" o-- "0..1" Facture : gère
FactureController "1" o-- "*" Facture : archive
ClientController "1" o-- "*" Client : gère
ItemController "1" o-- "*" Item : gère
Facture "1" *-- "*" LigneFacture : contient
Facture "1" --> "1" Client : pour
LigneFacture ..> Item : copie (snapshot)
Les Vues Spécialisées (Explosées)
En entreprise, on préfère "exploser" l'architecture en plusieurs petites vues thématiques pour mieux comprendre chaque zone.
A. Le Cœur du Modèle (La Facture)
Ce diagramme se concentre uniquement sur la structure d'une facture. On y voit clairement que la Facture possède des Lignes (Composition) et que la Ligne copie les données de l'item.
classDiagram
class Facture {
-Client _client
-List~LigneFacture~ _lignes
+CalculerTotal() : decimal
+LignesReadOnly : IReadOnlyList~LigneFacture~
}
class LigneFacture {
-string _description
-decimal _prixUnitaire
}
class Client {
-int _id
-string _nom
}
class Item {
-string _description
-decimal _prix
}
Facture "1" *-- "*" LigneFacture : contient
Facture "1" --> "1" Client : pour
LigneFacture ..> Item : copie (snapshot)
B. La Gestion des Clients
Ce diagramme montre comment le contrôleur gère la liste globale des clients. C'est ici que l'interface console ou WinForms vient chercher les clients existants.
classDiagram
class ClientController {
-List~Client~ _clients
+AjouterClient(int, string)
+ObtenirClientParId(int) : Client
+ClientsReadOnly : IReadOnlyList~Client~
}
class Client {
-int _id
-string _nom
}
ClientController "1" o-- "*" Client : gère la liste
C. La Gestion du Catalogue (Produits)
Même principe pour les produits : le contrôleur est le gardien de la liste des items disponibles.
classDiagram
class ItemController {
-List~Item~ _itemsDisponibles
+AjouterItemDisponible(string, decimal)
+CatalogueReadOnly : IReadOnlyList~Item~
}
class Item {
-string _description
-decimal _prix
}
ItemController "1" o-- "*" Item : gère le catalogue
D. Le Chef d'Orchestre (FactureController)
Enfin, le contrôleur de facture fait le lien. Il manipule l'objet Facture en cours et range les anciennes dans les archives.
classDiagram
class FactureController {
-Facture _factureEnCours
-List~Facture~ _historiqueFactures
+CreerNouvelleFacture(Client)
+AjouterItem(Item)
+TerminerFacture()
+HistoriqueReadOnly : IReadOnlyList~Facture~
}
class Facture {
+CalculerTotal() : decimal
}
FactureController "1" o-- "0..1" Facture : gère
FactureController "1" o-- "*" Facture : archive
7. Tests Unitaires : La Preuve par l'Action
C'est ici que le patron MVC montre toute sa puissance. Puisque le "cerveau" (le contrôleur et le modèle) est dans un projet à part (Lib), on peut le tester sans même lancer l'interface graphique.
Pourquoi tester sans interface ?
- Vitesse : Un test unitaire prend quelques millisecondes. Ouvrir une fenêtre, cliquer sur 3 boutons et vérifier le total prend 30 secondes.
- Fiabilité : On élimine les erreurs humaines de saisie.
- Isolation : Si le calcul est faux, on sait que c'est dans le Modèle, pas parce que le bouton a mal fonctionné.
La Méthodologie AAA
Chaque test doit suivre trois étapes claires :
- ARRANGER : On prépare les objets nécessaires (ex: un client, une facture).
- AGIR : On exécute l'action à tester (ex: calculer le total).
- AFFIRMER : On vérifie si le résultat est celui attendu (ex: est-ce bien 15.75$ ?).
[TestMethod]
public void CalculerTotal_DoitAdditionnerLesPrix() {
// 1. ARRANGER
Facture f = new Facture(new Client(1, "Test"));
f.AjouterItem(new Item("A", 10.50m));
f.AjouterItem(new Item("B", 5.25m));
// 2. AGIR
decimal total = f.CalculerTotal();
// 3. AFFIRMER
Assert.AreEqual(15.75m, total);
}
8. Méthodologie : Comment ajouter une fonction ?
Si vous devez ajouter une fonctionnalité (ex: Ajouter un client), ne foncez pas dans l'interface graphique tout de suite ! Suivez l'ordre logique :
- Le Modèle : Est-ce que mes données (
Client.cs) sont prêtes ? - Le Contrôleur : Est-ce que j'ai une méthode pour enregistrer (
AjouterClient) ? - Le Test : Est-ce que je peux vérifier que ça marche sans même ouvrir une fenêtre ?
- La Vue : Enfin, j'ajoute mon bouton et mon
try-catch.