Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 639 minIntermédiaire

Du diagramme de classes au code C#, et rétro-conception

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • traduire classes, associations et multiplicités en C# ;
  • coder un héritage, une interface, une composition et une classe-association ;
  • retrouver le diagramme de classes d'un code existant ;
  • écrire une méthode en suivant les associations d'un diagramme fourni.

4 exercices corrigés

Traduire un diagramme de classes en code, c'est appliquer des règles presque mécaniques : chaque classe devient une classe, chaque attribut un champ ou une propriété, chaque association une référence ou une collection selon sa multiplicité, chaque généralisation un héritage. La rétro-conception (reverse engineering) fait le chemin inverse : elle reconstitue le diagramme à partir d'un code existant, pour le comprendre ou le documenter. Ce chapitre donne les règles dans les deux sens, en C# 14 sur .NET 10, avec les équivalents TypeScript et PHP, et traduit en entier le diagramme d'AppliFrais.

Les règles de traduction

Élément UMLEn C#Exemple AppliFrais
Classepublic classpublic class FicheFrais
Classe abstraite, opération abstraiteabstract class, abstractpublic abstract string Role { get; }
Interface («interface»)public interfacepublic interface IExportable
Énumération («enumeration»)public enumpublic enum EtatFiche { Creee, … }
Attribut privé lu par d'autres classesPropriété { get; } ou { get; private set; }public EtatFiche Etat { get; private set; }
Attribut purement interneChamp privateprivate readonly List<…> lignesHorsForfait
Attribut [0..1]Type nullablepublic decimal? MontantValide
Attribut dérivé (/total)Propriété ou méthode calculéepublic decimal Total() => …
Attribut ou opération de classe (souligné)staticpublic static int NbFiches
OpérationMéthode, avec la même visibilitépublic void Cloturer()
Association de multiplicité 1Référence non nulle, reçue dans le constructeurpublic Visiteur Visiteur { get; }
Association de multiplicité 0..1Référence nullablepublic Comptable? ValideePar
Association de multiplicité *, 1..*Collection (List<T>), exposée en lecture seulepublic IReadOnlyList<FicheFrais> Fiches
Navigabilité dans un seul sensUne référence dans la classe d'où part la flèche seulementFicheFrais connaît FraisForfait, pas l'inverse
Généralisationclass Fille : Mereclass Visiteur : Utilisateur
Réalisationclass C : IInterfaceclass FicheFrais : IExportable
CompositionLe tout crée ses parties et ne les partage pasAjouterHorsForfait(…) crée la ligne
AgrégationLe tout reçoit une référence vers un objet créé ailleursregion.Ajouter(visiteur)
Classe-associationUne classe avec une référence vers chacune des deux classesLigneFraisForfait (fiche, forfait, quantité)

Trois points méritent qu'on s'y arrête.

Attribut ou propriété. En UML, l'attribut - mois : string est privé. En C#, on écrit rarement un champ privé accompagné d'un accesseur GetMois() : on écrit une propriété public string Mois { get; }, qui joue le même rôle (la donnée reste protégée, on ne peut que la lire). La règle à suivre : la visibilité UML de l'attribut décrit qui peut le modifier ; la propriété C# peut être lisible par tous et modifiable seulement par la classe (private set).

Les multiplicités deviennent des types. Un 1 devient une référence obligatoire, qu'on passe dans le constructeur pour qu'aucun objet n'existe sans elle (une fiche sans visiteur est impossible à construire). Un 0..1 devient un type nullable (Comptable?), que le compilateur t'oblige à tester. Un * devient une liste, que la classe garde privée et expose en lecture seule (IReadOnlyList<T>) : on n'ajoute une ligne que par une méthode qui vérifie les règles.

Les associations dans les deux sens. Si l'association est navigable dans les deux sens, chaque classe garde une référence vers l'autre, et le code doit garder les deux côtés cohérents : quand un visiteur ouvre une fiche, la fiche reçoit le visiteur et la liste du visiteur reçoit la fiche, dans la même méthode. C'est une raison de préférer une navigabilité dans un seul sens quand l'autre ne sert pas.

La traduction complète d'AppliFrais

Voici le code C# 14 (.NET 10) du diagramme de classes d'AppliFrais. Chaque type est dans son propre fichier, qui commence par namespace AppliFrais.Modele; ; seul le premier le montre.

L'énumération et l'interface d'abord :

csharp
// EtatFiche.cs
namespace AppliFrais.Modele;

// «enumeration» EtatFiche : la base stocke CR, CL, VA, MP, RB
public enum EtatFiche { Creee, Cloturee, Validee, MiseEnPaiement, Remboursee }

// IExportable.cs
public interface IExportable
{
    string VersCsv();
}

La classe abstraite Utilisateur et ses deux filles. Le constructeur de la classe mère est protected : seules les filles l'appellent. L'opération abstraite Role() devient une propriété abstraite, que chaque fille redéfinit avec override.

csharp
// Utilisateur.cs
public abstract class Utilisateur
{
    public int Id { get; }
    public string Nom { get; }
    public string Prenom { get; }
    public string Login { get; }

    protected Utilisateur(int id, string nom, string prenom, string login)
    {
        Id = id;
        Nom = nom;
        Prenom = prenom;
        Login = login;
    }

    public string NomComplet => $"{Prenom} {Nom}";

    public abstract string Role { get; }
}

// Visiteur.cs
public class Visiteur : Utilisateur
{
    private readonly List<FicheFrais> fiches = [];

    public DateOnly DateEmbauche { get; }
    public Region Region { get; private set; }            // agrégation : 1 côté Region
    public IReadOnlyList<FicheFrais> Fiches => fiches;    // association « remplit » : 0..*

    public Visiteur(int id, string nom, string prenom, string login, DateOnly dateEmbauche, Region region)
        : base(id, nom, prenom, login)
    {
        DateEmbauche = dateEmbauche;
        Region = region;
        region.Ajouter(this);                             // les deux côtés restent cohérents
    }

    public override string Role => "visiteur médical";

    public FicheFrais OuvrirFiche(string mois)
    {
        var fiche = new FicheFrais(mois, this);
        fiches.Add(fiche);
        return fiche;
    }
}

// Comptable.cs
public class Comptable : Utilisateur
{
    public Comptable(int id, string nom, string prenom, string login)
        : base(id, nom, prenom, login) { }

    public override string Role => "comptable";
}

La région agrège des visiteurs : elle reçoit des visiteurs créés ailleurs et ne les détruit pas. Le forfait est une classe simple, que les fiches utilisent sans qu'il les connaisse.

csharp
// Region.cs
public class Region
{
    private readonly List<Visiteur> visiteurs = [];

    public string Code { get; }
    public string Libelle { get; }
    public IReadOnlyList<Visiteur> Visiteurs => visiteurs;

    public Region(string code, string libelle)
    {
        Code = code;
        Libelle = libelle;
    }

    public void Ajouter(Visiteur visiteur) => visiteurs.Add(visiteur);
}

// FraisForfait.cs
public class FraisForfait
{
    public string Id { get; }          // ETP, KM, NUI, REP
    public string Libelle { get; }
    public decimal Montant { get; }

    public FraisForfait(string id, string libelle, decimal montant)
    {
        Id = id;
        Libelle = libelle;
        Montant = montant;
    }
}

La classe-association LigneFraisForfait devient une classe ordinaire qui tient une référence vers chacune des deux classes de l'association, plus son attribut quantite. La propriété Quantite utilise le mot-clé field, nouveauté de C# 14 : il désigne le champ caché de la propriété, ce qui permet de valider la valeur sans déclarer de champ à part.

csharp
// LigneFraisForfait.cs
public class LigneFraisForfait
{
    public FicheFrais Fiche { get; }
    public FraisForfait Frais { get; }

    public int Quantite
    {
        get;
        set => field = value >= 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value), "La quantité ne peut pas être négative.");
    }

    public LigneFraisForfait(FicheFrais fiche, FraisForfait frais, int quantite)
    {
        Fiche = fiche;
        Frais = frais;
        Quantite = quantite;
    }

    public decimal Montant() => Quantite * Frais.Montant;
}

La ligne hors forfait est une partie de la fiche (composition). Son constructeur est internal : le code des autres projets (l'interface graphique, par exemple) ne peut pas créer une ligne « en l'air », il doit passer par FicheFrais.AjouterHorsForfait(). La ligne ne connaît pas sa fiche : la composition est navigable dans un seul sens.

csharp
// LigneFraisHorsForfait.cs
public class LigneFraisHorsForfait
{
    public DateOnly Date { get; }
    public string Libelle { get; }
    public decimal Montant { get; }
    public bool Refuse { get; private set; }
    public string? MotifRefus { get; private set; }

    internal LigneFraisHorsForfait(DateOnly date, string libelle, decimal montant)
    {
        Date = date;
        Libelle = libelle;
        Montant = montant;
    }

    public void Refuser(string motif)
    {
        Refuse = true;
        MotifRefus = motif;
    }
}

Enfin FicheFrais, le cœur du modèle. Elle réalise IExportable, possède ses lignes (composition), connaît son visiteur (1), son validateur éventuel (0..1) et ses lignes forfaitisées (classe-association). Chaque opération qui change l'état vérifie d'abord l'état courant : c'est la traduction du diagramme d'états-transitions du chapitre 9.

csharp
// FicheFrais.cs
using System.Globalization;

public class FicheFrais : IExportable
{
    private readonly List<LigneFraisForfait> lignesForfait = [];
    private readonly List<LigneFraisHorsForfait> lignesHorsForfait = [];

    public string Mois { get; }
    public Visiteur Visiteur { get; }
    public EtatFiche Etat { get; private set; } = EtatFiche.Creee;
    public decimal? MontantValide { get; private set; }
    public Comptable? ValideePar { get; private set; }
    public IReadOnlyList<LigneFraisForfait> LignesForfait => lignesForfait;
    public IReadOnlyList<LigneFraisHorsForfait> LignesHorsForfait => lignesHorsForfait;

    public FicheFrais(string mois, Visiteur visiteur)
    {
        Mois = mois;
        Visiteur = visiteur;
    }

    public void SaisirForfait(FraisForfait frais, int quantite)
    {
        VerifierEtat(EtatFiche.Creee);
        LigneFraisForfait? ligne = lignesForfait.Find(l => l.Frais == frais);
        if (ligne is null) lignesForfait.Add(new LigneFraisForfait(this, frais, quantite));
        else ligne.Quantite = quantite;              // une seule ligne par forfait
    }

    public LigneFraisHorsForfait AjouterHorsForfait(DateOnly date, string libelle, decimal montant)
    {
        VerifierEtat(EtatFiche.Creee);
        var ligne = new LigneFraisHorsForfait(date, libelle, montant);
        lignesHorsForfait.Add(ligne);
        return ligne;
    }

    public decimal TotalForfait() => lignesForfait.Sum(l => l.Montant());

    public decimal TotalHorsForfait() => lignesHorsForfait.Where(l => !l.Refuse).Sum(l => l.Montant);

    public decimal Total() => TotalForfait() + TotalHorsForfait();

    public void Cloturer()
    {
        VerifierEtat(EtatFiche.Creee);
        Etat = EtatFiche.Cloturee;
    }

    public void Valider(Comptable comptable)
    {
        VerifierEtat(EtatFiche.Cloturee);
        MontantValide = Total();
        ValideePar = comptable;
        Etat = EtatFiche.Validee;
    }

    public void MettreEnPaiement()
    {
        VerifierEtat(EtatFiche.Validee);
        Etat = EtatFiche.MiseEnPaiement;
    }

    public void ConfirmerRemboursement()
    {
        VerifierEtat(EtatFiche.MiseEnPaiement);
        Etat = EtatFiche.Remboursee;
    }

    public string VersCsv() =>
        string.Join(";", Visiteur.Id, Mois, Etat, Total().ToString(CultureInfo.InvariantCulture));

    private void VerifierEtat(EtatFiche attendu)
    {
        if (Etat != attendu)
            throw new InvalidOperationException($"La fiche {Mois} est à l'état {Etat}, l'état {attendu} est attendu.");
    }
}

Un programme de test, qui reprend la fiche de novembre 2026 de Lucas :

csharp
// Program.cs
var ouest = new Region("OUE", "Ouest");
var lucas = new Visiteur(7, "Moreau", "Lucas", "lmoreau", new DateOnly(2021, 9, 1), ouest);
var ines = new Comptable(2, "Fabre", "Inès", "ifabre");

var etape = new FraisForfait("ETP", "Forfait étape", 110.00m);
var km = new FraisForfait("KM", "Frais kilométrique", 0.62m);
var nuitee = new FraisForfait("NUI", "Nuitée hôtel", 80.00m);
var repas = new FraisForfait("REP", "Repas restaurant", 25.00m);

FicheFrais fiche = lucas.OuvrirFiche("2026-11");
fiche.SaisirForfait(etape, 18);
fiche.SaisirForfait(km, 412);
fiche.SaisirForfait(nuitee, 3);
fiche.SaisirForfait(repas, 12);
fiche.AjouterHorsForfait(new DateOnly(2026, 11, 5), "Taxi gare - hôtel", 23.40m);
fiche.AjouterHorsForfait(new DateOnly(2026, 11, 12), "Repas avec un praticien", 48.50m);
LigneFraisHorsForfait cadeau = fiche.AjouterHorsForfait(new DateOnly(2026, 11, 19), "Cadeau à un praticien", 60.00m);

Console.WriteLine($"Forfait : {fiche.TotalForfait()} ; hors forfait : {fiche.TotalHorsForfait()} ; total : {fiche.Total()}");

try { fiche.Valider(ines); }
catch (InvalidOperationException e) { Console.WriteLine($"Erreur : {e.Message}"); }

fiche.Cloturer();
cadeau.Refuser("Cadeau interdit");
fiche.Valider(ines);
Console.WriteLine($"État : {fiche.Etat}, montant validé : {fiche.MontantValide}, par {fiche.ValideePar?.NomComplet}");

List<Utilisateur> utilisateurs = [lucas, ines];
foreach (Utilisateur u in utilisateurs) Console.WriteLine($"{u.NomComplet} : {u.Role}");

Exécuté avec dotnet run sur un poste réglé en français, il affiche :

text
Forfait : 2775,44 ; hors forfait : 131,90 ; total : 2907,34
Erreur : La fiche 2026-11 est à l'état Creee, l'état Cloturee est attendu.
État : Validee, montant validé : 2847,34, par Inès Fabre
Lucas Moreau : visiteur médical
Inès Fabre : comptable

Le forfait vaut 18 × 110 + 412 × 0,62 + 3 × 80 + 12 × 25 = 1 980 + 255,44 + 240 + 300 = 2 775,44 €. La validation d'une fiche encore ouverte est refusée, comme le veut la règle de gestion RG1. Une fois le cadeau refusé (60 €), le montant validé tombe à 2 907,34 − 60 = 2 847,34 €. La boucle finale appelle Role sans savoir si l'utilisateur est un visiteur ou un comptable : chacun répond avec sa propre redéfinition.

Les équivalents en TypeScript et en PHP

Le même diagramme se traduit dans tous les langages objet. Les mots changent, les règles restent.

UMLC#TypeScriptPHP 8.4
Généralisationclass Visiteur : Utilisateurclass Visiteur extends Utilisateurclass Visiteur extends Utilisateur
Réalisationclass FicheFrais : IExportableclass FicheFrais implements Exportableclass FicheFrais implements Exportable
Classe abstraiteabstract class Utilisateurabstract class Utilisateurabstract class Utilisateur
- privéprivateprivate (vérifié à la compilation) ou #champ (vérifié à l'exécution)private
# protégéprotectedprotectedprotected
Attribut en lecture seule{ get; }, readonlyreadonlyreadonly (ou private(set) en PHP 8.4)
0..1Comptable?Comptable | null?Comptable
*List<FicheFrais>FicheFrais[]array (avec un commentaire @var FicheFrais[])
Énumérationenum EtatFicheUnion de chaînes ou enumenum EtatFiche: string

Avec React et Node.js, tu écris souvent des objets simples et des fonctions plutôt que des classes ; le diagramme de classes reste utile pour décrire les données échangées (les types TypeScript) et les classes de l'API. Les classes, l'encapsulation et le polymorphisme sont détaillés dans le cours Programmation orientée objet, en C#.

La rétro-conception : du code au diagramme

La rétro-conception consiste à reconstituer le diagramme d'un code existant. Tu en as besoin quand tu reprends une application sans documentation (en stage, très souvent), quand tu dois la faire évoluer, ou quand un sujet d'examen te donne du code et te demande de le modéliser. La démarche :

  1. Liste les types : chaque class, abstract class, interface et enum devient un élément du diagramme, avec son stéréotype.
  2. Trace l'héritage et les réalisations : ce qui suit les deux-points (en C#), extends et implements (en TypeScript et PHP).
  3. Trie les champs et propriétés :
    • de type simple (int, string, decimal, DateOnly, une énumération) : un attribut ;
    • de type classe (Visiteur) : une association de multiplicité 1, ou 0..1 si le type est nullable (Comptable?) ;
    • de type collection (List<FicheFrais>) : une association de multiplicité * ;
    • la classe qui possède la référence est le point de départ de la navigabilité.
  4. Détermine la nature des associations : si un objet crée ses parties lui-même et ne les expose qu'en lecture, c'est une composition ; s'il reçoit des objets créés ailleurs, c'est une association (ou une agrégation).
  5. Garde les opérations métier : les méthodes publiques qui font quelque chose, avec leurs paramètres et leur type de retour. Laisse de côté les accesseurs triviaux et les détails techniques.
  6. Vérifie les multiplicités de l'autre côté : le code d'une classe ne dit pas combien d'objets la référencent. Cherche-le dans les règles de gestion, la base de données ou les usages.

Les outils de développement (Visual Studio pour le C#, PhpStorm, IntelliJ, des extensions de VS Code) savent générer un diagramme à partir du code. C'est pratique sur un gros projet, mais le diagramme généré montre tout, y compris les détails sans intérêt. Faire la rétro-conception à la main sur les trois ou quatre classes centrales t'oblige à les comprendre : c'est le meilleur moyen de reprendre une application. Le cours Maintenance applicative montre comment reprendre un projet existant, de la lecture du code au diagramme de séquence.

Écrire une méthode à partir du diagramme

C'est la question de code la plus fréquente dans les sujets d'examen SLAM : le sujet fournit le diagramme de classes (et parfois un extrait de code) et demande d'écrire une méthode. La méthode :

  1. repère la classe où écrire la méthode et les données dont elle a besoin ;
  2. trouve dans le diagramme le chemin des associations qui y mène, en suivant les multiplicités : un * signifie une boucle, un 0..1 un test de nullité ;
  3. utilise les opérations existantes plutôt que de recalculer : si Total() existe, appelle-la ;
  4. écris le code, puis relis-le en suivant le chemin.

Exemple : écrire dans Region une méthode TotalValide(mois : string) : decimal qui renvoie le total des montants validés des fiches de ce mois, pour tous les visiteurs de la région. Le chemin est Region → 0..* Visiteur → 0..* FicheFrais : deux boucles imbriquées. Le montant validé est 0..1 : on teste qu'il existe.

csharp
public decimal TotalValide(string mois)
{
    decimal total = 0;
    foreach (Visiteur visiteur in visiteurs)
    {
        foreach (FicheFrais fiche in visiteur.Fiches)
        {
            if (fiche.Mois == mois && fiche.MontantValide is not null)
            {
                total += fiche.MontantValide.Value;
            }
        }
    }
    return total;
}

Avec Lucas (2 847,34 € validés en novembre) et une collègue de la région Ouest dont la fiche de novembre a été validée à 1 300 €, ouest.TotalValide("2026-11") renvoie 4 147,34. Une fiche de décembre encore ouverte n'a pas de montant validé : TotalValide("2026-12") renvoie 0.

Exercices

Exercice 1 sur 4 Traduire un diagramme Réussi À revoir

Traduis en C# le diagramme suivant (constructeurs compris) :

  • Adherent : - numero : string, - nom : string ; opération + NbEmpruntsEnCours() : int ;
  • Livre : - isbn : string, - titre : string ;
  • Emprunt : - dateEmprunt : DateOnly, - dateRetour : DateOnly [0..1] ; opération + Rendre(jour : DateOnly) ;
  • Adherent "1" ──── "0..*" Emprunt, navigable dans les deux sens ;
  • Emprunt "0..*" ────▸ "1" Livre, navigable de l'emprunt vers le livre seulement.
Voir la réponseMasquer la réponse de l'exercice 1
csharp
public class Adherent
{
    private readonly List<Emprunt> emprunts = [];

    public string Numero { get; }
    public string Nom { get; }
    public IReadOnlyList<Emprunt> Emprunts => emprunts;

    public Adherent(string numero, string nom)
    {
        Numero = numero;
        Nom = nom;
    }

    public Emprunt Emprunter(Livre livre, DateOnly jour)
    {
        var emprunt = new Emprunt(this, livre, jour);
        emprunts.Add(emprunt);
        return emprunt;
    }

    public int NbEmpruntsEnCours() => emprunts.Count(e => e.DateRetour is null);
}

public class Livre
{
    public string Isbn { get; }
    public string Titre { get; }

    public Livre(string isbn, string titre)
    {
        Isbn = isbn;
        Titre = titre;
    }
}

public class Emprunt
{
    public Adherent Adherent { get; }
    public Livre Livre { get; }
    public DateOnly DateEmprunt { get; }
    public DateOnly? DateRetour { get; private set; }

    public Emprunt(Adherent adherent, Livre livre, DateOnly dateEmprunt)
    {
        Adherent = adherent;
        Livre = livre;
        DateEmprunt = dateEmprunt;
    }

    public void Rendre(DateOnly jour) => DateRetour = jour;
}

Les points qui comptent : Livre n'a aucune référence vers Emprunt (navigabilité dans un seul sens) ; dateRetour [0..1] devient DateOnly? ; les deux références de multiplicité 1 sont passées au constructeur d'Emprunt ; la liste d'Adherent est privée et exposée en lecture seule. La méthode Emprunter n'est pas dans le diagramme : elle garde les deux côtés de l'association cohérents, comme OuvrirFiche dans AppliFrais. Le constructeur d'Emprunt pourrait être internal pour obliger à passer par elle.

Tu avais trouvé ?

Exercice 2 sur 4 Rétro-conception Réussi À revoir

Voici un extrait du code d'une application de réservation de ressources partagées (salles, véhicules de service). Dessine le diagramme de classes correspondant (en texte : classes, attributs, opérations, relations et multiplicités).

csharp
public interface INotifiable { void Notifier(string message); }

public abstract class Ressource
{
    public string Code { get; }
    public string Nom { get; }
    protected Ressource(string code, string nom) { Code = code; Nom = nom; }
    public abstract decimal CoutHoraire();
}

public class Salle : Ressource
{
    public int Capacite { get; }
    public Salle(string code, string nom, int capacite) : base(code, nom) { Capacite = capacite; }
    public override decimal CoutHoraire() => 2.5m * Capacite;
}

public class VehiculeService : Ressource
{
    public string Immatriculation { get; }
    public VehiculeService(string code, string nom, string immatriculation) : base(code, nom)
    { Immatriculation = immatriculation; }
    public override decimal CoutHoraire() => 8m;
}

public class Employe : INotifiable
{
    private readonly List<Reservation> reservations = [];
    public string Matricule { get; }
    public string Nom { get; }
    public string Email { get; }
    public IReadOnlyList<Reservation> Reservations => reservations;
    public Employe(string matricule, string nom, string email) { Matricule = matricule; Nom = nom; Email = email; }
    internal void Ajouter(Reservation reservation) => reservations.Add(reservation);
    public void Notifier(string message) => Console.WriteLine($"Courriel à {Email} : {message}");
}

public class Rappel
{
    public int MinutesAvant { get; }
    public bool Envoye { get; private set; }
    internal Rappel(int minutesAvant) { MinutesAvant = minutesAvant; }
    internal void MarquerEnvoye() => Envoye = true;
}

public class Reservation
{
    private readonly List<Employe> participants = [];
    private readonly List<Rappel> rappels = [];
    public DateTime Debut { get; }
    public DateTime Fin { get; }
    public Ressource Ressource { get; }
    public Employe Organisateur { get; }

    public Reservation(DateTime debut, DateTime fin, Ressource ressource, Employe organisateur)
    {
        if (fin <= debut) throw new ArgumentException("La fin doit suivre le début.");
        Debut = debut; Fin = fin; Ressource = ressource; Organisateur = organisateur;
        organisateur.Ajouter(this);
    }
    public void Inviter(Employe employe) => participants.Add(employe);
    public void AjouterRappel(int minutesAvant) => rappels.Add(new Rappel(minutesAvant));
    public decimal Cout() => (decimal)(Fin - Debut).TotalHours * Ressource.CoutHoraire();
    public void EnvoyerRappels() { /* notifie chaque participant pour chaque rappel */ }
}
Voir la réponseMasquer la réponse de l'exercice 2

Types

  • «interface» INotifiable : + Notifier(message : string).
  • Ressource {abstract} : - code : string, - nom : string ; + CoutHoraire() : decimal {abstract}.
  • Salle : - capacite : int ; + CoutHoraire() : decimal. Généralisation vers Ressource.
  • VehiculeService : - immatriculation : string ; + CoutHoraire() : decimal. Généralisation vers Ressource.
  • Employe : - matricule : string, - nom : string, - email : string ; + Notifier(message : string). Réalisation de INotifiable.
  • Rappel : - minutesAvant : int, - envoye : bool.
  • Reservation : - debut : DateTime, - fin : DateTime ; + Inviter(employe : Employe), + AjouterRappel(minutesAvant : int), + Cout() : decimal, + EnvoyerRappels(). On peut ajouter la contrainte {fin > debut}.

Associations

  • Reservation "0..*" ────▸ "1" Ressource : une réservation porte sur une ressource. Navigable vers Ressource seulement (Ressource n'a pas de liste de réservations). Le 0..* côté Reservation ne se lit pas dans le code : c'est la règle métier (une ressource est réservée plusieurs fois).
  • Reservation "0..*" ──── "1" Employe, rôle organisateur côté Employe : navigable dans les deux sens (Organisateur d'un côté, Reservations de l'autre, tenus cohérents par le constructeur).
  • Reservation "0..*" ────▸ "0..*" Employe, rôle participants côté Employe : navigable vers Employe seulement. C'est une deuxième association entre les deux mêmes classes : les rôles les distinguent.
  • Reservation ◆────▸ "0..*" Rappel : composition. La réservation crée ses rappels (new Rappel dans AjouterRappel, constructeur internal), ne les partage pas, et ils n'ont pas de sens sans elle.

CoutHoraire() est redéfinie dans chaque fille : Cout() l'appelle sans savoir s'il s'agit d'une salle ou d'un véhicule (polymorphisme). Une salle de 12 places réservée de 9 h à 11 h coûte 2 h × 2,5 × 12 = 60 € ; un véhicule réservé 10 heures coûte 80 €.

Tu avais trouvé ?

Exercice 3 sur 4 Écrire une méthode à partir du diagramme Réussi À revoir

À partir du diagramme d'AppliFrais, écris dans FicheFrais la méthode NbLignesRefusees() : int, puis dans Visiteur la méthode TotalRembourse(annee : int) : decimal, qui renvoie la somme des montants validés des fiches de l'année donnée qui sont à l'état Remboursee. Le mois d'une fiche est une chaîne au format "2026-11".

Voir la réponseMasquer la réponse de l'exercice 3
csharp
// Dans FicheFrais
public int NbLignesRefusees()
{
    int nb = 0;
    foreach (LigneFraisHorsForfait ligne in lignesHorsForfait)
    {
        if (ligne.Refuse) nb++;
    }
    return nb;
}

// Dans Visiteur
public decimal TotalRembourse(int annee)
{
    decimal total = 0;
    foreach (FicheFrais fiche in fiches)
    {
        if (fiche.Mois.StartsWith($"{annee}-") && fiche.Etat == EtatFiche.Remboursee)
        {
            total += fiche.MontantValide ?? 0;
        }
    }
    return total;
}

Le chemin est court : la fiche parcourt ses propres lignes (composition, 0..*), le visiteur ses propres fiches (0..*). Une fiche remboursée a forcément un montant validé, mais MontantValide reste de type decimal? : ?? 0 traite le cas sans faire planter le programme. Avec LINQ, la seconde méthode s'écrit fiches.Where(f => f.Mois.StartsWith($"{annee}-") && f.Etat == EtatFiche.Remboursee).Sum(f => f.MontantValide ?? 0), une réponse tout aussi juste à l'examen.

Tu avais trouvé ?

Exercice 4 sur 4 Le code et le diagramme sont-ils cohérents ? Réussi À revoir

Un diagramme indique Commande "0..*" ──── "1" Client et Commande ◆──── "1..*" LigneCommande. Le code contient :

csharp
public class Commande
{
    public Client? Client { get; set; }
    public List<LigneCommande> Lignes { get; set; } = [];
}

public class LigneCommande
{
    public Produit Produit { get; set; }
    public int Quantite { get; set; }
    public Commande Commande { get; set; }
}

Relève les écarts entre le code et le diagramme.

Voir la réponseMasquer la réponse de l'exercice 4
  • Client? est nullable et modifiable à tout moment : le code autorise une commande sans client, alors que le diagramme impose 1. Il faut une propriété non nullable, en lecture seule, initialisée par le constructeur.
  • Lignes a un set public et la liste est modifiable de l'extérieur : n'importe qui peut remplacer ou partager les lignes, ce qui contredit la composition. Il faut une liste privée, exposée en IReadOnlyList, et une méthode AjouterLigne qui crée la ligne.
  • Rien ne garantit 1..* : une commande peut rester sans ligne. Le constructeur peut exiger une première ligne, ou une méthode Valider() peut refuser une commande vide.
  • LigneCommande a une propriété Commande modifiable : une ligne pourrait changer de commande. Dans une composition, la partie ne change pas de tout. Le compilateur signale d'ailleurs un problème voisin : Produit et Commande ne sont pas nullables mais aucun constructeur ne les initialise (avertissement CS8618).
  • Produit n'apparaît pas dans l'extrait du diagramme : soit le diagramme est incomplet, soit le code ajoute une association non documentée. Il faut compléter le diagramme.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Diagramme de séquence : lignes de vie, messages et fragmentsRevoir le sommaire du cours