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
- Association simple (A utilise B)
- Agrégation (A contient B, B peut exister sans A)
- Composition (A contient B, B ne peut pas exister sans A)
- Héritage (A est un B)
- Implémentation d’interface (A implémente I)
- 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 :
- Quelles classes devraient implémenter cette interface ?
- Comment calculer automatiquement la note moyenne ?
- Faut-il une pondération selon l’ancienneté des notes ?
- 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 :
- Listez toutes les violations (au moins 5)
- Pour chaque violation, expliquez le risque
- 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 :
- Pourquoi rendre certaines propriétés en lecture seule (get public, set private) ?
- Où effectuer la validation de l’ISBN : dans le setter ou dans une méthode dédiée ?
- Comment empêcher qu’on modifie
DateAjoutaprè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 :
- Pourquoi créer une interface
INotificationService? - Comment tester cette classe sans envoyer de vrais emails ?
- Créez une classe
ServiceNotificationSMSqui 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 :
- Créez
LoggerConsolequi affiche dans la console - Créez
LoggerMultiplequi peut logger dans plusieurs destinations simultanément - 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 :
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
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 :
- Ajoutez un nouveau type
LivreEnfant(21 jours, pas de pénalité) - Ajoutez
LivreNumerique(pas de limite de durée, pas de pénalité) - 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 :
- ✅ Facilite les tests unitaires (on peut injecter des mocks)
- ✅ Permet de changer d’implémentation sans modifier le code
- ✅ Respect du principe de substitution de Liskov
- ✅ 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.