Aller au contenu

É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 :

  1. 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.
  2. 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.
  3. 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  
}  
Le Contrôleur instancie alors un nouvel objet 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.

// Vue (WinForms)  
_factureCtrl.AjouterItem(itemSelectionne);  

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).

  1. Le Modèle détecte l'erreur (ex: prix négatif) et lance un cri d'alerte : throw new ArgumentException().
  2. 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 ?

  1. Vitesse : Un test unitaire prend quelques millisecondes. Ouvrir une fenêtre, cliquer sur 3 boutons et vérifier le total prend 30 secondes.
  2. Fiabilité : On élimine les erreurs humaines de saisie.
  3. 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 :

  1. ARRANGER : On prépare les objets nécessaires (ex: un client, une facture).
  2. AGIR : On exécute l'action à tester (ex: calculer le total).
  3. 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 :

  1. Le Modèle : Est-ce que mes données (Client.cs) sont prêtes ?
  2. Le Contrôleur : Est-ce que j'ai une méthode pour enregistrer (AjouterClient) ?
  3. Le Test : Est-ce que je peux vérifier que ça marche sans même ouvrir une fenêtre ?
  4. La Vue : Enfin, j'ajoute mon bouton et mon try-catch.