Genie logiciel igl235
Thème :
Junior
1 vues ·6/6 leçons

Corrig Travaux Dirigés et Pratiques - Programmation Orientée Objets C# (Version Détaillée et Corrigée)


📘 TD 1 : Modélisation POO et Diagrammes UML (3 heures)

🎯 Objectifs Pédagogiques

  • Maîtriser l’analyse orientée objet d’un système réel
  • Concevoir des diagrammes UML professionnels
  • Appliquer les principes d’encapsulation et d’héritage
  • Identifier et documenter les règles métier

📋 Contexte du TD

Vous êtes consultant technique pour ShareRide, une start-up de covoiturage qui souhaite moderniser son système de gestion. L’entreprise a besoin d’une architecture logicielle robuste, évolutive et conforme aux standards de la POO. Votre mission est de concevoir le modèle objet complet du système avant le développement.


📝 Exercice 1 : Analyse et Conception du Système (1h30)

Scénario détaillé : Système de covoiturage “ShareRide”

Le système doit gérer les entités et fonctionnalités suivantes :

A. Gestion des Utilisateurs

  • Chaque utilisateur possède :
    • Un identifiant unique (UUID)
    • Des informations personnelles (nom, prénom, date de naissance)
    • Un email (unique dans le système)
    • Un numéro de téléphone validé
    • Une photo de profil
    • Une date d’inscription
    • Un statut de compte (Actif, Suspendu, Banni)
  • Un utilisateur peut être :
    • Conducteur : avec permis de conduire (numéro + date de validité), années d’expérience
    • Passager : avec préférences de voyage (musique, conversation, animaux)
    • Les deux à la fois (un utilisateur peut créer des trajets ET réserver)

B. Gestion des Véhicules

  • Un véhicule appartient à un conducteur
  • Caractéristiques :
    • Marque, modèle, année de fabrication
    • Immatriculation (unique)
    • Nombre de places disponibles (2 à 8)
    • Couleur
    • Catégorie (Citadine, Berline, SUV, Van)
    • Consommation moyenne (L/100km)
    • Options de confort (climatisation, WiFi, prises USB)
  • Un conducteur peut posséder plusieurs véhicules
  • Un véhicule doit être vérifié et approuvé par l’administration

C. Gestion des Trajets

  • Un trajet est créé par un conducteur pour un véhicule spécifique
  • Informations obligatoires :
    • Adresse de départ (complète avec coordonnées GPS)
    • Adresse d’arrivée
    • Date et heure de départ
    • Date et heure estimée d’arrivée
    • Distance en kilomètres
    • Prix par place (en euros)
    • Nombre de places disponibles
  • Informations optionnelles :
    • Points d’arrêt intermédiaires
    • Options du trajet (bagages autorisés, fumeur/non-fumeur, animaux acceptés)
    • Flexibilité horaire (+/- 15 minutes)
  • États possibles : Planifié → En cours → Terminé → Annulé
  • Un trajet peut avoir des contraintes particulières (ex : réservé aux femmes, non-fumeur uniquement)

D. Gestion des Réservations

  • Une réservation lie un passager à un trajet
  • Informations :
    • Date/heure de la réservation
    • Nombre de places réservées (1 à 4 maximum)
    • Point de départ précis (peut différer légèrement de l’adresse du trajet)
    • Point d’arrivée précis
    • Prix total payé
    • Statut (En attente, Confirmée, Annulée, Terminée)
  • États de réservation :
    • En attente : Le conducteur doit confirmer
    • Confirmée : Réservation validée par le conducteur
    • Annulée : Par le passager ou le conducteur
    • Terminée : Trajet effectué

E. Système de Notation et Avis

  • Après un trajet terminé, chaque participant peut noter les autres
  • Une notation comprend :
    • Une note sur 5 étoiles
    • Un commentaire textuel (optionnel, 500 caractères max)
    • Des critères spécifiques :
      • Pour un conducteur : conduite, ponctualité, amabilité, propreté du véhicule
      • Pour un passager : ponctualité, amabilité, respect
    • Date de la notation
  • La note moyenne d’un utilisateur est calculée automatiquement
  • Les avis sont modérés (peuvent être signalés pour contenu inapproprié)

Questions de l’Exercice 1 (à traiter dans l’ordre)

Question 1.1 : Identification des Classes (30 minutes)

Identifiez toutes les classes nécessaires pour modéliser ce système. Pour chaque classe, indiquez :

  • Son nom (selon la convention PascalCase)
  • Sa responsabilité principale en une phrase
  • Sa catégorie (Entité, Contrôleur, Service, Enumération)

Exemple de réponse attendue :

Classe : Utilisateur
Responsabilité : Représente une personne inscrite sur la plateforme
Catégorie : Entité

Indices pour la réflexion :

  • Y a-t-il des classes abstraites nécessaires ?
  • Quelles énumérations (enum) seraient utiles ?
  • Existe-t-il des classes d’association ?

Question 1.2 : Définition des Attributs et Méthodes (40 minutes)

Pour chaque classe identifiée, définissez :

A. Les Attributs

  • Nom de l’attribut
  • Type de données (int, string, DateTime, decimal, etc.)
  • Visibilité (private, protected, public)
  • Valeur par défaut (si applicable)

Format attendu pour les attributs :

Classe : Vehicule
- immatriculation : string (private, obligatoire, unique)
- marque : string (private, obligatoire)
- modele : string (private, obligatoire)
- nombrePlaces : int (private, valeur entre 2 et 8)
- annee : int (private)
- consommation : decimal (private, en L/100km)
- statut : StatutVehicule (private, par défaut: EnAttente)
- dateCreation : DateTime (private, par défaut: DateTime.Now)

B. Les Propriétés Définissez les propriétés publiques avec getters/setters appropriés :

// Exemple pour la classe Vehicule
public string Immatriculation
{
    get => immatriculation;
    private set => immatriculation = value; // Lecture publique, modification privée
}

public int NombrePlaces
{
    get => nombrePlaces;
    set
    {
        if (value < 2 || value > 8)
            throw new ArgumentException("Le nombre de places doit être entre 2 et 8");
        nombrePlaces = value;
    }
}

C. Les Méthodes Pour chaque classe, listez les méthodes principales avec leur signature complète :

Format attendu :

Classe : Trajet
Méthodes :
1. bool PeutEtreReserve()
   - Retourne true si le trajet accepte encore des réservations
   - Vérifie : places disponibles > 0, statut == Planifié, date dans le futur

2. void AnnulerTrajet(string motif)
   - Change le statut à Annulé
   - Notifie tous les passagers ayant réservé
   - Enregistre le motif d'annulation

3. decimal CalculerPrixTotal()
   - Calcule le prix en fonction de la distance et des options
   - Applique les majorations (nuit, péage, etc.)

4. List<PointArret> ObtenirItineraire()
   - Retourne l'itinéraire complet avec tous les points d'arrêt

Contraintes à respecter :

  • Toutes les données sensibles doivent être privées
  • Les méthodes de modification doivent valider les données
  • Chaque méthode doit avoir une responsabilité unique
  • Les méthodes qui peuvent échouer doivent lever des exceptions appropriées

Question 1.3 : Relations entre Classes (30 minutes)

Identifiez et documentez toutes les relations entre les classes :

A. Types de Relations à Identifier

  1. Association simple (A utilise B)
  2. Agrégation (A contient B, B peut exister sans A)
  3. Composition (A contient B, B ne peut pas exister sans A)
  4. Héritage (A est un B)
  5. Implémentation d’interface (A implémente I)
  6. Dépendance (A dépend de B)

Format attendu :

Relation : Conducteur → Vehicule
Type : Agrégation
Cardinalité : 1 Conducteur possède 0..* Véhicules
Description : Un conducteur peut posséder plusieurs véhicules,
              un véhicule appartient à un seul conducteur
Navigation : Bidirectionnelle

Questions spécifiques :

  • Quelle est la relation entre Utilisateur et Conducteur ?
  • Comment modéliser qu’un utilisateur peut être conducteur ET passager ?
  • Faut-il une classe d’association entre Trajet et Passager ?
  • Comment gérer la relation entre Notation et les utilisateurs notant/notés ?

Question 1.4 : Diagramme de Classes UML Complet (20 minutes)

Dessinez le diagramme de classes UML complet incluant :

Éléments obligatoires du diagramme :

  • [ ] Toutes les classes avec leurs noms
  • [ ] Attributs avec types et visibilité (-, +, #)
  • [ ] Méthodes avec paramètres et types de retour
  • [ ] Relations avec cardinalités
  • [ ] Interfaces (si utilisées)
  • [ ] Classes abstraites (si utilisées)
  • [ ] Énumérations

Conventions UML à respecter :

Visibilité :
- private   : -
+ public    : +
# protected : #

Types de relations :
→  Association
◆→ Agrégation (diamant vide)
◆→ Composition (diamant plein)
▷  Héritage (flèche triangulaire vide)
▷  Implémentation (flèche triangulaire pointillée)

Outils recommandés :

  • draw.io (gratuit, en ligne)
  • Lucidchart
  • Visual Paradigm Community Edition
  • PlantUML (pour les diagrammes en code)

Question 1.5 : Opportunités d’Héritage et Polymorphisme (10 minutes)

Identifiez et justifiez les cas d’utilisation d’héritage :

A. Hiérarchie à analyser : Utilisateur

Questions :
1. Faut-il créer une classe de base Utilisateur abstraite ?
2. Comment modéliser le fait qu'un utilisateur peut être conducteur ET passager ?
3. Proposez deux approches différentes :
   - Approche 1 : Héritage (Conducteur extends Utilisateur)
   - Approche 2 : Composition (Utilisateur has Profil Conducteur)
4. Quelle approche recommandez-vous et pourquoi ?

B. Hiérarchie à analyser : Véhicule

Questions :
1. Les différentes catégories de véhicules justifient-elles un héritage ?
2. Proposez une hiérarchie :
   Vehicule (classe de base)
   ├─ VehiculeTourisme (2-5 places)
   ├─ VehiculeUtilitaire (6-8 places)
   └─ VehiculeConfort (options premium)
3. Quelles méthodes seraient virtuelles/abstraites ?

Format de réponse attendu :

Hiérarchie : Utilisateur
Structure proposée :
- Utilisateur (classe abstraite)
  ├─ Conducteur : Utilisateur
  └─ Passager : Utilisateur

Justification :
[Expliquez pourquoi cette structure est pertinente]

Avantages :
1. [Avantage 1]
2. [Avantage 2]

Inconvénients :
1. [Inconvénient 1]
2. [Inconvénient 2]

Recommandation finale :
[Votre choix argumenté]

Question 1.6 : Interface INotable (10 minutes)

Concevez l’interface INotable pour standardiser le système de notation.

Spécifications :

interface INotable
{
    // Propriétés
    double NoteMoyenne { get; } // Lecture seule, calculée automatiquement
    int NombreNotations { get; } // Nombre total de notations reçues

    // Méthodes
    void AjouterNotation(Notation notation);
    List<Notation> ObtenirNotations();
    List<Notation> ObtenirNotations(int nombreDernieres);
    bool PeutEtreNote(); // Vérifie si l'objet peut recevoir une notation
    void SupprimerNotation(int idNotation); // Pour modération
}

Questions :

  1. Quelles classes devraient implémenter cette interface ?
  2. Comment calculer automatiquement la note moyenne ?
  3. Faut-il une pondération selon l’ancienneté des notes ?
  4. Comment gérer les notes signalées comme inappropriées ?

📝 Exercice 2 : Règles Métier et Encapsulation (1h30)

Objectif

Traduire les contraintes métier en code robuste et maintenable en respectant les principes d’encapsulation.


Partie A : Validation des Réservations (30 minutes)

Règle 1 : Vérification de la disponibilité

Règle métier :
Un passager ne peut pas réserver plus de places qu'il n'y en a de disponibles.

Questions :
1. Dans quelle classe placer cette validation ?
   a) Classe Reservation
   b) Classe Trajet
   c) Classe ServiceReservation
   Justifiez votre choix.

2. Écrivez la signature de la méthode de validation :
   bool PeutReserver(int nombrePlaces)

3. Quels attributs faut-il vérifier ?
   - Places totales du véhicule
   - Places déjà réservées
   - Places en attente de confirmation

4. Quelle exception lever en cas d'échec ?
   PlacesIndisponiblesException ou ArgumentException ?

Code attendu :

public class Trajet
{
    private int placesDisponibles;
    private List<Reservation> reservations;

    public bool PeutReserver(int nombrePlacesVoulues)
    {
        // TODO: Implémenter la logique
        // 1. Calculer les places déjà réservées confirmées
        // 2. Soustraire du total de places disponibles
        // 3. Vérifier si assez de places restent
        // 4. Retourner true/false
    }

    public void ReserverPlaces(Passager passager, int nombrePlaces)
    {
        // TODO: Utiliser PeutReserver avant de créer la réservation
        // Lever une exception appropriée si impossible
    }
}

Règle 2 : Auto-réservation interdite

Règle métier :
Un conducteur ne peut pas réserver son propre trajet.

Questions :
1. À quel moment effectuer cette vérification ?
   - À la création de la réservation
   - Avant l'affichage du formulaire de réservation
   - Les deux ?

2. Signature de la méthode :
   bool EstConducteurDuTrajet(Utilisateur utilisateur)

3. Comment comparer les identifiants utilisateurs de manière sûre ?

Code attendu :

public class ServiceReservation
{
    public Reservation CreerReservation(Trajet trajet, Utilisateur passager, int places)
    {
        // TODO: Vérifier que passager != trajet.Conducteur
        // TODO: Lever une exception ReservationInterditException si c'est le cas
        // TODO: Créer et retourner la réservation
    }
}

Règle 3 : Délai minimum de réservation

Règle métier :
Une réservation ne peut pas être faite moins de 2 heures avant le départ du trajet.

Questions :
1. Comment calculer la différence entre maintenant et la date de départ ?
2. Quelle précision temporelle utiliser (minutes, heures) ?
3. Faut-il arrondir le délai vers le haut ou le bas ?
4. Comment gérer les fuseaux horaires ?

Code attendu :

public class Trajet
{
    private DateTime dateDepart;

    public bool EstReservableMaintenant()
    {
        // TODO: Calculer la différence entre DateTime.Now et dateDepart
        // TODO: Vérifier si >= 2 heures
        // TODO: Gérer le cas où le trajet est dans le passé
    }

    public TimeSpan TempsRestantAvantDepart()
    {
        // TODO: Retourner le temps restant
    }
}

Partie B : Calcul des Prix (30 minutes)

Règle 1 : Prix de base par kilomètre

Contexte :
Prix de base = Distance (km) × Tarif par km
Tarif par km varie selon la catégorie du véhicule :
- Citadine : 0.40 €/km
- Berline : 0.50 €/km
- SUV : 0.60 €/km
- Van : 0.55 €/km

Questions :
1. Où stocker ces tarifs ?
   - En dur dans le code
   - Dans une base de données
   - Dans un fichier de configuration

2. Comment gérer l'évolution des tarifs dans le temps ?

3. Écrivez la méthode :
   decimal CalculerPrixBase()

Code attendu :

public enum CategorieVehicule
{
    Citadine,
    Berline,
    SUV,
    Van
}

public class CalculateurPrix
{
    private readonly Dictionary<CategorieVehicule, decimal> tarifsParKm = new()
    {
        { CategorieVehicule.Citadine, 0.40m },
        { CategorieVehicule.Berline, 0.50m },
        { CategorieVehicule.SUV, 0.60m },
        { CategorieVehicule.Van, 0.55m }
    };

    public decimal CalculerPrixBase(Trajet trajet)
    {
        // TODO: Récupérer la catégorie du véhicule du trajet
        // TODO: Multiplier distance × tarif
        // TODO: Arrondir à 2 décimales
    }
}

Règle 2 : Majoration de nuit

Règle métier :
Majoration de 20% pour les trajets de nuit (22h-6h).
Un trajet est considéré "de nuit" si le départ OU l'arrivée sont entre 22h et 6h.

Questions :
1. Comment déterminer si une heure est dans la plage nocturne ?
2. Faut-il considérer uniquement l'heure de départ, ou aussi l'arrivée ?
3. Comment gérer un trajet qui traverse minuit ?

Code attendu :
bool EstTrajetDeNuit(DateTime depart, DateTime arrivee)
decimal AppliquerMajorationNuit(decimal prixBase, bool estNuit)

Règle 3 : Réduction fidélité

Règle métier :
Réduction de 10% si le passager a une note moyenne > 4.5/5 et plus de 10 trajets effectués.

Questions :
1. Cette logique appartient-elle à :
   - La classe Passager
   - La classe CalculateurPrix
   - Une classe ServiceFidelite

2. Comment éviter de recalculer la note moyenne à chaque fois ?

3. Méthode attendue :
   decimal AppliquerReductionFidelite(Passager passager, decimal prix)

Partie C : Gestion des États (30 minutes)

Machine à États pour Trajet

États possibles :
1. Planifié (état initial)
2. EnCours (quand le trajet a commencé)
3. Terminé (quand le trajet est fini)
4. Annulé (par le conducteur ou système)

Transitions autorisées :
Planifié → EnCours (manuel par conducteur)
Planifié → Annulé (avant le départ)
EnCours → Terminé (à l'arrivée)
EnCours → Annulé (en cas de problème grave)

Transitions interdites :
Terminé → [tout autre état] (irréversible)
Annulé → [tout autre état] (irréversible)

Questions :

Q1 : Implémentation avec Enum

public enum StatutTrajet
{
    Planifie,
    EnCours,
    Termine,
    Annule
}

public class Trajet
{
    private StatutTrajet statut;

    // TODO: Implémenter les méthodes de transition
    public void Demarrer()
    {
        // Vérifier que statut == Planifie
        // Changer vers EnCours
    }

    public void Terminer()
    {
        // TODO: Implémenter
    }

    public void Annuler(string motif)
    {
        // TODO: Implémenter
    }
}

Q2 : Implémentation avec Pattern State

Créez une hiérarchie de classes pour chaque état :

interface IEtatTrajet
{
    void Demarrer(Trajet trajet);
    void Terminer(Trajet trajet);
    void Annuler(Trajet trajet, string motif);
}

class EtatPlanifie : IEtatTrajet { }
class EtatEnCours : IEtatTrajet { }
class EtatTermine : IEtatTrajet { }
class EtatAnnule : IEtatTrajet { }

Q3 : Comparaison des approches

Tableau comparatif :

| Critère              | Enum Simple | Pattern State |
|---------------------|-------------|---------------|
| Facilité d'implémentation |         |               |
| Maintenabilité      |             |               |
| Extensibilité       |             |               |
| Testabilité         |             |               |
| Performance         |             |               |

Recommandation : [Choisissez et justifiez]

Partie D : Encapsulation et Propriétés (Synthèse)

Identifiez les violations d’encapsulation dans ce code :

public class Trajet
{
    public string PointDepart;
    public string PointArrivee;
    public DateTime DateDepart;
    public decimal Prix;
    public int PlacesDisponibles;
    public List<Reservation> Reservations;

    public void AjouterReservation(Reservation res)
    {
        Reservations.Add(res);
        PlacesDisponibles--;
    }
}

Questions :

  1. Listez toutes les violations (au moins 5)
  2. Pour chaque violation, expliquez le risque
  3. Proposez une version corrigée avec :
    • Champs privés
    • Propriétés publiques appropriées
    • Validation dans les setters
    • Méthodes métier encapsulées

🔧 TP 1 : Refactoring et Principes SOLID (3 heures)

🎯 Objectifs du TP

  • Identifier les violations des principes SOLID dans du code legacy
  • Refactorer progressivement vers une architecture propre
  • Appliquer les design patterns appropriés
  • Comprendre l’impact des choix architecturaux sur la maintenabilité

📋 Contexte Professionnel

Vous venez d’être embauché dans une entreprise de logiciels pour bibliothèques. Le système actuel fonctionne mais souffre de problèmes graves :

  • Bugs fréquents lors des modifications
  • Impossibilité d’ajouter de nouvelles fonctionnalités sans casser l’existant
  • Tests automatisés impossibles à écrire
  • Code copié-collé partout

Votre mission : refactorer le code en appliquant les principes SOLID, sans changer les fonctionnalités existantes.


📦 Code de Départ Complet (à analyser puis refactorer)

using System;
using System.Collections.Generic;
using System.IO;

namespace BibliothequeAncien
{
    // ⚠️ CODE À REFACTORER - NE PAS UTILISER EN PRODUCTION ⚠️

    public class Livre
    {
        // Tous les champs publics - VIOLATION D'ENCAPSULATION
        public string Titre;
        public string Auteur;
        public string ISBN;
        public bool EstEmprunte;
        public DateTime? DateEmprunt;
        public string EmprunteurNom;
        public string EmprunteurEmail;
        public int NombreEmpruntsTotal;
        public DateTime DateAjout;

        // Constructeur
        public Livre(string titre, string auteur, string isbn)
        {
            Titre = titre;
            Auteur = auteur;
            ISBN = isbn;
            EstEmprunte = false;
            NombreEmpruntsTotal = 0;
            DateAjout = DateTime.Now;
        }

        // Méthode qui fait TOUT - VIOLATION du SRP
        public void Emprunter(string nom, string email)
        {
            if (!EstEmprunte)
            {
                // Validation du nom
                if (string.IsNullOrWhiteSpace(nom))
                {
                    Console.WriteLine("Erreur : nom invalide");
                    return;
                }

                // Validation de l'email
                if (!email.Contains("@"))
                {
                    Console.WriteLine("Erreur : email invalide");
                    return;
                }

                // Vérifier le quota (max 3 livres par personne)
                int livresEmpruntes = CompterLivresEmpruntes(email);
                if (livresEmpruntes >= 3)
                {
                    Console.WriteLine($"Quota atteint pour {email}");
                    return;
                }

                // Modification de l'état
                EstEmprunte = true;
                DateEmprunt = DateTime.Now;
                EmprunteurNom = nom;
                EmprunteurEmail = email;
                NombreEmpruntsTotal++;

                // Envoi d'email - RESPONSABILITÉ MAL PLACÉE
                EnvoyerEmailConfirmation(email, nom);

                // Logging - RESPONSABILITÉ MAL PLACÉE
                LoggerEmprunt();

                // Calculer et afficher la pénalité potentielle - RESPONSABILITÉ MAL PLACÉE
                CalculerEtAfficherPenalite();
            }
            else
            {
                Console.WriteLine($"Le livre '{Titre}' est déjà emprunté");
            }
        }

        // Méthode privée pour compter les livres - COUPLAGE FORT
        private int CompterLivresEmpruntes(string email)
        {
            // Simule l'accès à une base de données
            // En réalité, devrait être dans un Repository
            return 2; // Valeur en dur pour l'exemple
        }

        // Envoi d'email dans la classe métier - VIOLATION SRP
        private void EnvoyerEmailConfirmation(string email, string nom)
        {
            Console.WriteLine($"--- EMAIL ENVOYÉ ---");
            Console.WriteLine($"À: {email}");
            Console.WriteLine($"Sujet: Confirmation d'emprunt");
            Console.WriteLine($"Bonjour {nom},");
            Console.WriteLine($"Vous avez emprunté: {Titre}");
            Console.WriteLine($"Date de retour: {DateEmprunt?.AddDays(14):dd/MM/yyyy}");
            Console.WriteLine($"-------------------");

            // Si on voulait envoyer un vrai email ou SMS,
            // il faudrait modifier cette classe - VIOLATION Open/Closed
        }

        // Logging dans la classe métier - VIOLATION SRP
        private void LoggerEmprunt()
        {
            string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] " +
                              $"EMPRUNT - Livre: {ISBN} - Emprunteur: {EmprunteurEmail}";
            Console.WriteLine($"LOG: {logMessage}");

            // En production, devrait écrire dans un fichier ou une DB
            try
            {
                File.AppendAllText("biblio.log", logMessage + Environment.NewLine);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Erreur log: {ex.Message}");
            }
        }

        // Calcul de pénalité dans la classe métier - VIOLATION SRP
        private void CalculerEtAfficherPenalite()
        {
            if (DateEmprunt.HasValue)
            {
                int joursEmprunt = (DateTime.Now - DateEmprunt.Value).Days;
                decimal penalite = 0;

                if (joursEmprunt > 14)
                {
                    int joursRetard = joursEmprunt - 14;
                    penalite = joursRetard * 0.50m; // 0.50€ par jour de retard
                }

                Console.WriteLine($"Pénalité potentielle après 14 jours: {penalite:C}");
            }
        }

      EstEmprunte)
            {
                int joursEmprunt = (DateTime.Now - DateEmprunt.Value).Days;
                decimal penalite = 0;

                if (joursEmprunt > 14)
                {
                    int joursRetard = joursEmprunt - 14;
                    penalite = joursRetard * 0.50m;
                    Console.WriteLine($"PÉNALITÉ À PAYER: {penalite:C}");

                    // Envoi email de pénalité
                    Console.WriteLine($"Email de pénalité envoyé à {EmprunteurEmail}");
                }

                // Logging
                string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] " +
                                  $"RETOUR - Livre: {ISBN} - Durée: {joursEmprunt} jours - Pénalité: {penalite:C}";
                Console.WriteLine($"LOG: {logMessage}");
                File.AppendAllText("biblio.log", logMessage + Environment.NewLine);

                // Réinitialisation
                EstEmprunte = false;
                DateEmprunt = null;
                EmprunteurNom = null;
                EmprunteurEmail = null;
            }
        }

        // Méthode d'affichage - RESPONSABILITÉ MAL PLACÉE
        public void AfficherDetails()
        {
            Console.WriteLine($"=== {Titre} ===");
            Console.WriteLine($"Auteur: {Auteur}");
            Console.WriteLine($"ISBN: {ISBN}");
            Console.WriteLine($"Statut: {(EstEmprunte ? "Emprunté" : "Disponible")}");
            if (EstEmprunte)
            {
                Console.WriteLine($"Emprunté par: {EmprunteurNom}");
                Console.WriteLine($"Date d'emprunt: {DateEmprunt:dd/MM/yyyy}");
                int joursRestants = 14 - (DateTime.Now - DateEmprunt.Value).Days;
                Console.WriteLine($"Jours restants: {joursRestants}");
            }
            Console.WriteLine($"Nombre total d'emprunts: {NombreEmpruntsTotal}");
        }
    }

    // Classe principale pour tester
    class Program
    {
        static void Main(string[] args)
        {
            Livre livre1 = new Livre("Clean Code", "Robert C. Martin", "978-0132350884");
            livre1.Emprunter("Alice Dupont", "alice@email.com");
            livre1.AfficherDetails();

            Console.WriteLine("\n--- 15 jours plus tard ---\n");
            livre1.Retourner();
        }
    }
}

📋 Analyse des Violations (à compléter par les étudiants)

Tableau d’analyse à remplir :

Ligne de Code Principe Violé Description du Problème Impact Gravité (1-5)
public string Titre; Encapsulation Champ public modifiable Intégrité données compromise 4
Méthode Emprunter() SRP Fait validation, emprunt, email, log, calcul Impossible à tester/maintenir 5
EnvoyerEmailConfirmation() SRP + OCP Notification dans classe métier Changement = modification classe 4
… … … … …

🔨 Partie 1 : Encapsulation et Validation (45 minutes)

Objectif

Corriger toutes les violations d’encapsulation et centraliser la validation.

Tâche 1.1 : Création des Propriétés (20 min)

Transformez tous les champs publics en propriétés avec validation appropriée.

Code attendu :

public class Livre
{
    // Champs privés
    private string titre;
    private string auteur;
    private string isbn;
    private bool estEmprunte;
    private DateTime? dateEmprunt;
    private string emprunteurNom;
    private string emprunteurEmail;
    private int nombreEmpruntsTotal;
    private readonly DateTime dateAjout;

    // Propriété Titre avec validation
    public string Titre
    {
        get => titre;
        private set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Le titre ne peut pas être vide", nameof(Titre));
            if (value.Length > 200)
                throw new ArgumentException("Le titre ne peut pas dépasser 200 caractères", nameof(Titre));
            titre = value;
        }
    }

    // TODO: Créer les propriétés pour :
    // - Auteur (validation similaire au titre)
    // - ISBN (format ISBN-13: doit contenir exactement 13 chiffres)
    // - EstEmprunte (lecture seule publique, modification privée)
    // - DateEmprunt (lecture seule, peut être null)
    // - EmprunteurNom (lecture seule)
    // - EmprunteurEmail (lecture seule)
    // - NombreEmpruntsTotal (lecture seule)
    // - DateAjout (lecture seule)
}

Questions :

  1. Pourquoi rendre certaines propriétés en lecture seule (get public, set private) ?
  2. Où effectuer la validation de l’ISBN : dans le setter ou dans une méthode dédiée ?
  3. Comment empêcher qu’on modifie DateAjout après la création ?

Tâche 1.2 : Validation de l’ISBN (15 min)

L’ISBN-13 est composé de 13 chiffres. Créez une méthode de validation robuste.

public static class ValidateurISBN
{
    public static bool EstISBNValide(string isbn)
    {
        // TODO:
        // 1. Supprimer les tirets et espaces
        // 2. Vérifier que la longueur est 13
        // 3. Vérifier que tous les caractères sont des chiffres
        // 4. (Bonus) Vérifier la clé de contrôle ISBN-13

        // Format attendu: 978-0-13-235088-4 ou 9780132350884
    }
}

Tests à écrire :

// Cas valides
ValidateurISBN.EstISBNValide("9780132350884")         // true
ValidateurISBN.EstISBNValide("978-0-13-235088-4")     // true

// Cas invalides
ValidateurISBN.EstISBNValide("978013235088")          // false (trop court)
ValidateurISBN.EstISBNValide("978013235088ABC")       // false (lettres)
ValidateurISBN.EstISBNValide(null)                    // false
ValidateurISBN.EstISBNValide("")                      // false

Tâche 1.3 : Constructeur avec Validation (10 min)

Modifiez le constructeur pour utiliser les propriétés validées.

public Livre(string titre, string auteur, string isbn)
{
    // Utiliser les propriétés (pas les champs)
    // Les setters feront la validation automatiquement
    Titre = titre;
    Auteur = auteur;

    if (!ValidateurISBN.EstISBNValide(isbn))
        throw new ArgumentException($"ISBN invalide : {isbn}", nameof(isbn));

    this.isbn = isbn;
    estEmprunte = false;
    nombreEmpruntsTotal = 0;
    dateAjout = DateTime.Now;
}

🔨 Partie 2 : Single Responsibility Principle (45 minutes)

Objectif

Séparer les responsabilités en créant des classes dédiées.

Tâche 2.1 : Extraction de la Notification (15 min)

Créez une classe dédiée à l’envoi de notifications.

// Interface pour le principe Dependency Inversion (sera utilisé en Partie 4)
public interface INotificationService
{
    void EnvoyerConfirmationEmprunt(string destinataire, string nomUtilisateur,
                                    string titreLivre, DateTime dateRetourPrevu);
    void EnvoyerNotificationPenalite(string destinataire, string nomUtilisateur,
                                     string titreLivre, decimal montantPenalite);
}

// Implémentation concrète pour les emails
public class ServiceNotificationEmail : INotificationService
{
    public void EnvoyerConfirmationEmprunt(string email, string nom, string livre, DateTime dateRetour)
    {
        // TODO: Implémenter l'envoi réel d'email
        // Pour l'instant, on simule avec Console.WriteLine
        Console.WriteLine($"--- EMAIL ENVOYÉ ---");
        Console.WriteLine($"À: {email}");
        Console.WriteLine($"Sujet: Confirmation d'emprunt");
        Console.WriteLine($"Bonjour {nom},");
        Console.WriteLine($"Vous avez emprunté: {livre}");
        Console.WriteLine($"Date de retour: {dateRetour:dd/MM/yyyy}");
        Console.WriteLine($"-------------------");
    }

    public void EnvoyerNotificationPenalite(string email, string nom, string livre, decimal penalite)
    {
        // TODO: Implémenter
        Console.WriteLine($"--- EMAIL PÉNALITÉ ---");
        Console.WriteLine($"À: {email}");
        Console.WriteLine($"Montant à payer: {penalite:C}");
        Console.WriteLine($"----------------------");
    }
}

Questions :

  1. Pourquoi créer une interface INotificationService ?
  2. Comment tester cette classe sans envoyer de vrais emails ?
  3. Créez une classe ServiceNotificationSMS qui implémente la même interface

Tâche 2.2 : Extraction du Logging (15 min)

Créez un système de logging professionnel.

public enum NiveauLog
{
    Info,
    Warning,
    Error
}

public interface ILogger
{
    void Log(string message, NiveauLog niveau = NiveauLog.Info);
    void LogEmprunt(string isbn, string emprunteur);
    void LogRetour(string isbn, int dureeJours, decimal penalite);
}

public class LoggerFichier : ILogger
{
    private readonly string cheminFichier;

    public LoggerFichier(string chemin = "biblio.log")
    {
        cheminFichier = chemin;
    }

    public void Log(string message, NiveauLog niveau = NiveauLog.Info)
    {
        string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        string niveauStr = niveau.ToString().ToUpper();
        string ligneLog = $"[{timestamp}] [{niveauStr}] {message}";

        try
        {
            File.AppendAllText(cheminFichier, ligneLog + Environment.NewLine);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Erreur lors du logging : {ex.Message}");
        }
    }

    public void LogEmprunt(string isbn, string emprunteur)
    {
        Log($"EMPRUNT - ISBN: {isbn} - Emprunteur: {emprunteur}");
    }

    public void LogRetour(string isbn, int duree, decimal penalite)
    {
        Log($"RETOUR - ISBN: {isbn} - Durée: {duree} jours - Pénalité: {penalite:C}");
    }
}

Améliorations à implémenter :

  1. Créez LoggerConsole qui affiche dans la console
  2. Créez LoggerMultiple qui peut logger dans plusieurs destinations simultanément
  3. Ajoutez une rotation des fichiers de log (créer un nouveau fichier chaque jour)

Tâche 2.3 : Extraction du Calcul de Pénalité (15 min)

Créez une classe dédiée aux calculs financiers.

public interface ICalculateurPenalite
{
    decimal CalculerPenalite(DateTime dateEmprunt, DateTime dateRetour);
}

public class CalculateurPenaliteStandard : ICalculateurPenalite
{
    private const int JOURS_AUTORISES = 14;
    private const decimal TARIF_PAR_JOUR = 0.50m;

    public decimal CalculerPenalite(DateTime dateEmprunt, DateTime dateRetour)
    {
        int joursEmprunt = (dateRetour - dateEmprunt).Days;

        if (joursEmprunt <= JOURS_AUTORISES)
            return 0m;

        int joursRetard = joursEmprunt - JOURS_AUTORISES;
        decimal penalite = joursRetard * TARIF_PAR_JOUR;

        // Arrondir à 2 décimales
        return Math.Round(penalite, 2);
    }
}

Extensions à créer :

  1. CalculateurPenaliteProgressif : tarif augmente avec le retard
    • 0-7 jours de retard : 0.50€/jour
    • 8-14 jours de retard : 0.75€/jour
    • 15+ jours de retard : 1.00€/jour
  2. CalculateurPenalitePlafonnee : maximum 50€ de pénalité

🔨 Partie 3 : Open/Closed Principle (45 minutes)

Objectif

Rendre le système extensible sans modifier le code existant.

Tâche 3.1 : Hiérarchie de Livres (25 min)

Créez une structure qui permet d’ajouter facilement de nouveaux types de livres.

// Classe abstraite de base
public abstract class Livre
{
    // Propriétés communes (déjà créées en Partie 1)
    public string Titre { get; protected set; }
    public string Auteur { get; protected set; }
    public string ISBN { get; protected set; }
    public bool EstEmprunte { get; protected set; }

    // Méthodes abstraites - chaque type définit son comportement
    public abstract int DureeEmpruntMaxJours { get; }
    public abstract bool PeutEtreEmprunte { get; }
    public abstract decimal CalculerPenalite(DateTime dateEmprunt);

    // Méthode commune
    public bool EstEnRetard()
    {
        if (!EstEmprunte || !DateEmprunt.HasValue)
            return false;

        int joursEcoules = (DateTime.Now - DateEmprunt.Value).Days;
        return joursEcoules > DureeEmpruntMaxJours;
    }
}

// Types concrets de livres
public class LivreStandard : Livre
{
    public override int DureeEmpruntMaxJours => 14;
    public override bool PeutEtreEmprunte => true;

    public override decimal CalculerPenalite(DateTime dateEmprunt)
    {
        int jours = (DateTime.Now - dateEmprunt).Days;
        if (jours <= DureeEmpruntMaxJours)
            return 0m;

        int joursRetard = jours - DureeEmpruntMaxJours;
        return joursRetard * 0.50m;
    }
}

public class LivreBestSeller : Livre
{
    public override int DureeEmpruntMaxJours => 7;  // Durée courte
    public override bool PeutEtreEmprunte => true;

    public override decimal CalculerPenalite(DateTime dateEmprunt)
    {
        // Pénalité plus élevée pour encourager le retour rapide
        int jours = (DateTime.Now - dateEmprunt).Days;
        if (jours <= DureeEmpruntMaxJours)
            return 0m;

        int joursRetard = jours - DureeEmpruntMaxJours;
        return joursRetard * 1.00m;  // 1€ par jour
    }
}

public class LivreReference : Livre
{
    public override int DureeEmpruntMaxJours => 0;  // Consultation sur place uniquement
    public override bool PeutEtreEmprunte => false;

    public override decimal CalculerPenalite(DateTime dateEmprunt)
    {
        throw new InvalidOperationException("Un livre de référence ne peut pas être emprunté");
    }
}

Questions :

  1. Ajoutez un nouveau type LivreEnfant (21 jours, pas de pénalité)
  2. Ajoutez LivreNumerique (pas de limite de durée, pas de pénalité)
  3. Avez-vous dû modifier les classes existantes ?

Tâche 3.2 : Factory Pattern (20 min)

Créez une factory pour instancier le bon type de livre.

public enum TypeLivre
{
    Standard,
    BestSeller,
    Reference,
    Enfant,
    Numerique
}

public class LivreFactory
{
    public static Livre CreerLivre(TypeLivre type, string titre, string auteur, string isbn)
    {
        return type switch
        {
            TypeLivre.Standard => new LivreStandard(titre, auteur, isbn),
            TypeLivre.BestSeller => new LivreBestSeller(titre, auteur, isbn),
            TypeLivre.Reference => new LivreReference(titre, auteur, isbn),
            TypeLivre.Enfant => new LivreEnfant(titre, auteur, isbn),
            TypeLivre.Numerique => new LivreNumerique(titre, auteur, isbn),
            _ => throw new ArgumentException($"Type de livre inconnu : {type}")
        };
    }

    // Surcharge avec détection automatique du type
    public static Livre CreerLivre(string titre, string auteur, string isbn,
                                   bool estBestSeller = false, bool estReference = false)
    {
        if (estReference)
            return new LivreReference(titre, auteur, isbn);
        if (estBestSeller)
            return new LivreBestSeller(titre, auteur, isbn);

        return new LivreStandard(titre, auteur, isbn);
    }
}

Utilisation :

// Création explicite
Livre livre1 = LivreFactory.CreerLivre(TypeLivre.BestSeller, "Clean Code", "R.C. Martin", "978...");

// Création avec flags
Livre livre2 = LivreFactory.CreerLivre("Design Patterns", "GoF", "978...", estBestSeller: true);

🔨 Partie 4 : Dependency Inversion Principle (45 minutes)

Objectif

Dépendre des abstractions (interfaces), pas des implémentations concrètes.

Tâche 4.1 : Injection de Dépendances (25 min)

Refactorez la classe ServiceEmprunt pour utiliser l’injection de dépendances.

Avant (couplage fort) :

public class ServiceEmprunt
{
    public void Emprunter(Livre livre, string nom, string email)
    {
        // Dépendances créées directement dans la classe
        var notificationService = new ServiceNotificationEmail();
        var logger = new LoggerFichier();
        var calculateur = new CalculateurPenaliteStandard();

        // Utilisation...
    }
}

Après (couplage faible) :

public class ServiceEmprunt
{
    private readonly INotificationService notificationService;
    private readonly ILogger logger;
    private readonly ICalculateurPenalite calculateurPenalite;
    private readonly IValidateurQuota validateurQuota;

    // Injection par constructeur
    public ServiceEmprunt(
        INotificationService notificationService,
        ILogger logger,
        ICalculateurPenalite calculateurPenalite,
        IValidateurQuota validateurQuota)
    {
        this.notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
        this.calculateurPenalite = calculateurPenalite ?? throw new ArgumentNullException(nameof(calculateurPenalite));
        this.validateurQuota = validateurQuota ?? throw new ArgumentNullException(nameof(validateurQuota));
    }

    public void Emprunter(Livre livre, string nomEmprunteur, string emailEmprunteur)
    {
        try
        {
            // Validation
            if (!livre.PeutEtreEmprunte)
                throw new InvalidOperationException($"Le livre '{livre.Titre}' ne peut pas être emprunté");

            if (livre.EstEmprunte)
                throw new InvalidOperationException($"Le livre '{livre.Titre}' est déjà emprunté");

            if (!validateurQuota.PeutEmprunter(emailEmprunteur))
                throw new InvalidOperationException($"Quota d'emprunt atteint pour {emailEmprunteur}");

            // Emprunt
            livre.Emprunter(nomEmprunteur, emailEmprunteur);

            // Notification
            DateTime dateRetour = DateTime.Now.AddDays(livre.DureeEmpruntMaxJours);
            notificationService.EnvoyerConfirmationEmprunt(
                emailEmprunteur, nomEmprunteur, livre.Titre, dateRetour);

            // Logging
            logger.LogEmprunt(livre.ISBN, emailEmprunteur);
        }
        catch (Exception ex)
        {
            logger.Log($"Erreur lors de l'emprunt : {ex.Message}", NiveauLog.Error);
            throw;
        }
    }

    public void Retourner(Livre livre)
    {
        // TODO: Implémenter avec les mêmes principes
    }
}

Avantages de cette approche :

  1. ✅ Facilite les tests unitaires (on peut injecter des mocks)
  2. ✅ Permet de changer d’implémentation sans modifier le code
  3. ✅ Respect du principe de substitution de Liskov
  4. ✅ Code plus flexible et maintenable

Tâche 4.2 : Création du Validateur de Quota (10 min)

public interface IValidateurQuota
{
    bool PeutEmprunter(string emailUtilisateur);
    int ObtenirNombreLivresEmpruntes(string emailUtilisateur);
}

public class ValidateurQuotaSimple : IValidateurQuota
{
    private const int QUOTA_MAX = 3;
    private readonly IRepositoryLivre repositoryLivre;

    public ValidateurQuotaSimple(IRepositoryLivre repository)
    {
        repositoryLivre = repository;
    }

    public bool PeutEmprunter(string email)
    {
        int nombreLivres = ObtenirNombreLivresEmpruntes(email);
        return nombreLivres < QUOTA_MAX;
    }

    public int ObtenirNombreLivresEmpruntes(string email)
    {
        // Délègue au repository
        return repositoryLivre.CompterLivresEmpruntesPar(email);
    }
}

// Version avancée avec quotas personnalisés
public class ValidateurQuotaPersonnalise : IValidateurQuota
{
    private readonly Dictionary<string, int> quotasPersonnalises;
    private readonly IRepositoryLivre repositoryLivre;
    private const int QUOTA_PAR_DEFAUT = 3;

    public ValidateurQuotaPersonnalise(IRepositoryLivre repository)
    {
        repositoryLivre = repository;
        quotasPersonnalises = new Dictionary<string, int>
        {
            { "etudiant@ecole.fr", 5 },    // Étudiants : 5 livres
            { "prof@ecole.fr", 10 },        // Professeurs : 10 livres
            { "chercheur@ecole.fr", 15 }    // Chercheurs : 15 livres
        };
    }

    public bool PeutEmprunter(string email)
    {
        int quota = ObtenirQuota(email);
        int nombreLivres = repositoryLivre.CompterLivresEmpruntesPar(email);
        return nombreLivres < quota;
    }

    private int ObtenirQuota(string email)
    {
        // Recherche par domaine email
        string domaine = email.Split('@').LastOrDefault();

        foreach (var (pattern, quota) in quotasPersonnalises)
        {
            if (email.EndsWith(pattern.Split('@')[1]))
                return quota;
        }

        return QUOTA_PAR_DEFAUT;
    }

    public int ObtenirNombreLivresEmpruntes(string email)
    {
        return repositoryLivre.CompterLivresEmpruntesPar(email);
    }
}

Tâche 4.3 : Démonstration de Substitution (10 min)

Montrez comment on peut facilement remplacer l’implémentation email par SMS.

// Nouvelle implémentation SMS
public class ServiceNotificationSMS : INotificationService
{
    public void EnvoyerConfirmationEmprunt(string telephone, string nom, string livre, DateTime dateRetour)
    {
        string message = $"Bonjour {nom}, vous avez emprunté '{livre}'. " +
                        $"Retour avant le {dateRetour:dd/MM/yyyy}";
        EnvoyerSMS(telephone, message);
    }

    public void EnvoyerNotificationPenalite(string telephone, string nom, string livre, decimal montant)
    {
        string message = $"Livre '{livre}' en retard. Pénalité : {montant:C}";
        EnvoyerSMS(telephone, message);
    }

    private void EnvoyerSMS(string numero, string message)
    {
        Console.WriteLine($"📱 SMS vers {numero}: {message}");
        // Intégration réelle avec un service SMS (Twilio, etc.)
    }
}

// Service hybride (Email ET SMS)
public class ServiceNotificationHybride : INotificationService
{
    private readonly ServiceNotificationEmail serviceEmail;
    private readonly ServiceNotificationSMS serviceSMS;

    public ServiceNotificationHybride()
    {
        serviceEmail = new ServiceNotificationEmail();
        serviceSMS = new ServiceNotificationSMS();
    }

    public void EnvoyerConfirmationEmprunt(string contact, string nom, string livre, DateTime dateRetour)
    {
        // Détermine si c'est un email ou un téléphone
        if (contact.Contains("@"))
        {
            serviceEmail.EnvoyerConfirmationEmprunt(contact, nom, livre, dateRetour);
        }
        else
        {
            serviceSMS.EnvoyerConfirmationEmprunt(contact, nom, livre, dateRetour);
        }
    }

    public void EnvoyerNotificationPenalite(string contact, string nom, string livre, decimal montant)
    {
        // Envoie les deux pour les pénalités (important)
        if (contact.Contains("@"))
        {
            serviceEmail.EnvoyerNotificationPenalite(contact, nom, livre, montant);
        }
        serviceSMS.EnvoyerNotificationPenalite(contact, nom, livre, montant);
    }
}

Programme principal (démonstration) :

class Program
{
    static void Main(string[] args)
    {
        // Configuration des dépendances
        INotificationService notificationService = new ServiceNotificationEmail();
        // OU : new ServiceNotificationSMS();
        // OU : new ServiceNotificationHybride();

        ILogger logger = new LoggerFichier("biblio.log");
        // OU : new LoggerConsole();
        // OU : new LoggerMultiple(new LoggerFichier(), new LoggerConsole());

        ICalculateurPenalite calculateurPenalite = new CalculateurPenaliteStandard();
        // OU : new CalculateurPenaliteProgressif();
        // OU : new CalculateurPenalitePlafonnee();

        IRepositoryLivre repositoryLivre = new RepositoryLivreEnMemoire();
        // OU : new RepositoryLivreMySQL(connectionString);

        IValidateurQuota validateurQuota = new ValidateurQuotaSimple(repositoryLivre);
        // OU : new ValidateurQuotaPersonnalise(repositoryLivre);

        // Création du service avec injection des dépendances
        ServiceEmprunt serviceEmprunt = new ServiceEmprunt(
            notificationService,
            logger,
            calculateurPenalite,
            validateurQuota
        );

        // Utilisation
        Livre livre = LivreFactory.CreerLivre(
            TypeLivre.Standard,
            "Clean Code",
            "Robert C. Martin",
            "9780132350884"
        );

        serviceEmprunt.Emprunter(livre, "Alice Dupont", "alice@email.com");

        Console.WriteLine("\n✅ Changement facile d'implémentation :");
        Console.WriteLine("Il suffit de changer la ligne de création du service !");
        Console.WriteLine("Aucune modification du code métier nécessaire.");
    }
}

📊 Livrables du TP 1

1. Code Refactoré Complet (70%)

Structure attendue :

BibliothequeRefactoree/
├── Models/
│   ├── Livre.cs (classe abstraite)
│   ├── LivreStandard.cs
│   ├── LivreBestSeller.cs
│   ├── LivreReference.cs
│   └── LivreFactory.cs
├── Services/
│   ├──

Prêt à passer à la suite ?

Vous avez terminé cette leçon sur Corrig Travaux Dirigés et Pratiques - Programmation Orientée Objets C# (Version Détaillée et Corrigée). Continuez votre apprentissage avec la prochaine leçon ou explorez d'autres formations.