Montée en compétence auto-rythmée - C#
📚 Module 2 : Programmation Orientée Objet
🔗 Prérequis : Module 1 - Fondamentaux C# ✅
Objectifs
- Concevoir et implémenter des classes robustes
- Maîtriser l'encapsulation et l'héritage
- Comprendre le polymorphisme et les interfaces
- Appliquer les principes SOLID de base
Contenu théorique
- Classes et objets : constructeurs, destructeurs, membres statiques
- Encapsulation : properties, access modifiers (private, public, protected, internal)
- Héritage : base classes, override, virtual, sealed
- Polymorphisme : méthodes virtuelles, classes abstraites
- Interfaces : contrats, implémentation multiple
- Principes SOLID : Single Responsibility, Open/Closed
Exemple rapide
// Classe de base avec encapsulation
public class Person
{
private string _name;
public string Name
{
get => _name;
set => _name = !string.IsNullOrWhiteSpace(value) ? value : throw new ArgumentException("Name cannot be empty");
}
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
// Méthode virtuelle : peut être redéfinie dans les classes filles
public virtual void Introduce()
{
Console.WriteLine($"Je suis {Name}, {Age} ans");
}
}
// Héritage et polymorphisme
public class Employee : Person
{
public decimal Salary { get; set; }
public Employee(string name, int age, decimal salary) : base(name, age)
{
Salary = salary;
}
// Redéfinition de la méthode de la classe parent
public override void Introduce()
{
base.Introduce(); // Appel du comportement parent
Console.WriteLine($"Mon salaire est {Salary:C}");
}
}
// Interface : contrat que doivent respecter les classes
public interface IPayable
{
decimal CalculateSalary(); // Calcul du salaire
}
📖 Ressources théoriques
- Object-Oriented Programming (C#) - Microsoft Learn
- Classes and Objects - Microsoft Learn
- Inheritance in C# - Microsoft Learn
🎥 Vidéos recommandées
- Object-Oriented Programming - Microsoft Learn
- OOP with derived or abstract classes, overrides - Microsoft Learn
🎯 Projet fil rouge : TechEvent Manager - Phase 2
📋 Contexte
Rappel Module 1 : Vous avez créé une calculatrice console pour analyser la viabilité des événements.
Phase 2 : Il est temps de structurer le domaine métier avec des classes ! L'ESN veut gérer différents types d'événements (workshops, conférences, meetups), chacun avec ses propres règles de tarification et de gestion.
Objectif : Créer un modèle objet robuste qui représente le domaine des événements techniques.
💻 Exercice pratique : Modèle objet TechEvent Manager
Objectif : Concevoir une architecture orientée objet complète avec héritage, interfaces et encapsulation.
📐 Diagramme de classes cible
┌─────────────┐
│ <<abstract>> │
│ Event │
├─────────────┤
│ +Id │
│ +Title │
│ +Date │
│ +Capacity │
│ -attendees │
├─────────────┤
│ +Register() │
│ +IsFull() │
│ +GetPrice()* │
└──────┬──────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Workshop│ │Conference│ │ Meetup │
├─────────┤ ├─────────┤ ├─────────┤
│ +Level │ │ +Topics │ │ +IsFree │
└─────────┘ └─────────┘ └─────────┘
┌──────────────┐
│ <<interface>>│
│ IBookable │
├──────────────┤
│ +Register() │
│ +Cancel() │
│ +IsFull() │
└──────────────┘
🏗️ Architecture à implémenter :
// Interface pour les événements réservables
public interface IBookable
{
bool Register(Participant participant);
bool Cancel(Participant participant);
bool IsFull();
int GetAvailableSeats();
}
// Classe abstraite de base pour tous les événements
public abstract class Event : IBookable
{
// Propriétés publiques
public int Id { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public int Capacity { get; set; }
public string Location { get; set; }
// Encapsulation : liste privée des participants
private List<Participant> _attendees;
// Propriété calculée (read-only)
public int AttendeesCount => _attendees.Count;
// Constructeur
protected Event(int id, string title, DateTime date, int capacity, string location)
{
// TODO: Initialiser toutes les propriétés et _attendees
}
// Méthode abstraite : chaque type d'événement a son propre prix
public abstract decimal GetPrice();
// Méthode virtuelle : comportement par défaut pour l'inscription
public virtual bool Register(Participant participant)
{
// TODO: Vérifier si complet, si déjà inscrit (même email), puis ajouter
// Cette méthode sera redéfinie dans Meetup pour autoriser le surbooking
}
public virtual bool Cancel(Participant participant)
{
// TODO: Retirer le participant de la liste
}
public bool IsFull()
{
// TODO: Comparer le nombre d'inscrits avec la capacité
}
public int GetAvailableSeats()
{
// TODO: Calculer les places restantes
}
public List<Participant> GetAttendees()
{
// TODO: Retourner une copie de la liste pour préserver l'encapsulation
}
public virtual void DisplayInfo()
{
// TODO (Optionnel): Afficher les informations de l'événement
}
}
// Classes dérivées : types d'événements spécifiques
public class Workshop : Event
{
public string Level { get; set; } // Beginner, Intermediate, Advanced
public int Duration { get; set; } // En heures
public Workshop(int id, string title, DateTime date, int capacity, string location, string level, int duration)
: base(id, title, date, capacity, location)
{
// TODO: Initialiser Level et Duration
}
public override decimal GetPrice()
{
// TODO: 25€/h * Duration, avec réduction/majoration selon Level
// Beginner: -20%, Intermediate: tarif normal, Advanced: +50%
}
public override void DisplayInfo()
{
// TODO (Optionnel): Afficher les infos + Level et Duration
}
}
public class Conference : Event
{
public List<string> Topics { get; set; }
public bool IncludesLunch { get; set; }
public Conference(int id, string title, DateTime date, int capacity, string location, bool includesLunch)
: base(id, title, date, capacity, location)
{
// TODO: Initialiser Topics et IncludesLunch
}
public override decimal GetPrice()
{
// TODO: 150€ de base, +30€ si IncludesLunch
}
public override void DisplayInfo()
{
// TODO (Optionnel): Afficher les infos + Topics et IncludesLunch
}
}
public class Meetup : Event
{
public bool IsFree => true; // Les meetups sont toujours gratuits
public Meetup(int id, string title, DateTime date, int capacity, string location)
: base(id, title, date, capacity, location)
{
// Pas de propriétés supplémentaires
}
public override decimal GetPrice()
{
// TODO: Retourner 0m (gratuit)
}
public override bool Register(Participant participant)
{
// TODO: Autoriser le surbooking jusqu'à 110% de la capacité
}
public override void DisplayInfo()
{
// TODO (Optionnel): Afficher les infos + mention "Free event"
}
}
// Classe Participant (fournie)
public class Participant
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Company { get; set; }
public Participant(int id, string name, string email, string company = "")
{
Id = id;
Name = name;
Email = email;
Company = company;
}
}
// Gestionnaire d'événements
public class EventManager
{
private List<Event> _events = new List<Event>();
public void AddEvent(Event evt)
{
// TODO: Vérifier si un événement avec le même Id existe, sinon ajouter
// Lever InvalidOperationException si doublon
}
public bool RemoveEvent(int eventId)
{
// TODO: Trouver et supprimer l'événement par ID
}
public Event GetEventById(int id)
{
// TODO: Rechercher l'événement par ID
}
public List<Event> GetAllEvents()
{
// TODO: Retourner une copie de la liste
}
public List<Event> GetUpcomingEvents()
{
// TODO: Filtrer les événements futurs et trier par date
}
public decimal GetTotalRevenue()
{
// TODO: Calculer le revenu total (prix * participants pour chaque événement)
}
public int GetTotalAttendees()
{
// TODO: Sommer tous les participants
}
public void DisplayAllEvents()
{
// TODO (Optionnel): Afficher tous les événements
}
}
📋 Fonctionnalités à implémenter
F1 - Gestion des événements
- Créer différents types d'événements (Workshop, Conference, Meetup)
- Ajouter un événement au système →
EventManager.AddEvent() - Supprimer un événement par ID →
EventManager.RemoveEvent() - Récupérer tous les événements →
EventManager.GetAllEvents() - Filtrer les événements à venir →
EventManager.GetUpcomingEvents()
F2 - Gestion des inscriptions (IBookable)
- Inscrire un participant →
event.Register(Participant) - Annuler une inscription →
event.Cancel(Participant) - Vérifier si complet →
event.IsFull() - Compter les places disponibles →
event.GetAvailableSeats() - Empêcher les doublons (même email)
F3 - Calcul des prix (Polymorphisme)
- Workshop : Prix basé sur niveau + durée
- Beginner: 20€/h, Intermediate: 25€/h, Advanced: 37.50€/h
- Conference : Prix fixe 150€ (+30€ si lunch inclus)
- Meetup : Gratuit (0€)
F4 - Règles métier spécifiques
- Workshop : Capacité stricte (pas de surbooking)
- Conference : Validation stricte de capacité
- Meetup : Surbooking autorisé (+10% de la capacité)
F5 - Statistiques et rapports
- Revenu total estimé →
EventManager.GetTotalRevenue() - Nombre total de participants →
EventManager.GetTotalAttendees() - Affichage des infos par type d'événement →
DisplayInfo()polymorphe
⚠️ Points d'attention
Encapsulation :
- ✅ Liste
_attendeesprivée → accès uniquement via méthodes - ✅ Propriété calculée
AttendeesCount(read-only) - ✅
GetAttendees()retourne une copie (pas la liste originale)
Héritage :
- ✅ Classe abstraite
Event→ code commun partagé - ✅ Classes dérivées (
Workshop,Conference,Meetup) → spécialisations - ✅ Constructeur
protecteddansEvent→ appelé viabase()
Polymorphisme :
- ✅
GetPrice()abstraite → chaque type calcule différemment - ✅
DisplayInfo()virtuelle → comportement par défaut + redéfinition - ✅
Register()virtuelle → surbooking uniquement pour Meetup
Interface :
- ✅
IBookable→ contrat pour les inscriptions - ✅ Implémentée dans
Event→ tous les types réservables
SOLID Principles :
- ✅ SRP : Event gère événements, Participant gère participants, EventManager gère collection
- ✅ OCP : Nouveau type d'événement = nouvelle classe (pas de modification d'Event)
- ✅ LSP : Workshop/Conference/Meetup utilisables partout où Event est attendu
- ✅ ISP : IBookable simple et focused
- ✅ DIP : EventManager dépend de l'abstraction Event, pas des types concrets
✅ Critères d'acceptance
Technique :
- Code compile sans erreur ni warning
- Classe abstraite
Eventavec méthodeGetPrice()abstraite - Interface
IBookableimplémentée correctement - Les 3 types d'événements héritent de
Event - Encapsulation :
_attendeesprivée, pas d'accès direct - Propriété calculée
AttendeesCount(read-only) - Validation : pas de doublons (même email)
Fonctionnel :
- Workshop : prix selon niveau et durée (formules correctes)
- Conference : prix 150€ + 30€ si lunch
- Meetup : gratuit + surbooking 110%
- Inscription refuse si événement complet
- Annulation fonctionne correctement
- EventManager gère plusieurs types d'événements
Tests unitaires :
- Au minimum 6 tests essentiels qui passent
- Coverage ≥ 70% sur les classes métier
- Tests de polymorphisme (GetPrice retourne valeurs différentes)
- Tests d'encapsulation (Register/Cancel fonctionnent correctement)
Livrables :
2-Module-POO/
├── TechEventManager/
│ ├── Models/
│ │ ├── Event.cs
│ │ ├── Workshop.cs
│ │ ├── Conference.cs
│ │ ├── Meetup.cs
│ │ ├── Participant.cs
│ │ └── IBookable.cs
│ ├── Services/
│ │ └── EventManager.cs
│ └── TechEventManager.csproj
└── TechEventManager.Tests/
├── EventTests.cs
├── WorkshopTests.cs
└── TechEventManager.Tests.csproj
🧪 Tests unitaires
Objectif : Valider encapsulation, héritage, polymorphisme et interfaces.
Format : 2 exemples fournis + 4 à implémenter vous-même
📝 Exemples fournis (2 tests)
Voici 2 exemples complets de tests de polymorphisme :
[Theory]
[InlineData("Beginner", 4, 80)] // 4h * 25€ * 0.8 = 80€
[InlineData("Intermediate", 4, 100)] // 4h * 25€ = 100€
[InlineData("Advanced", 4, 150)] // 4h * 25€ * 1.5 = 150€
public void Workshop_GetPrice_CalculatesCorrectly(string level, int duration, decimal expected)
{
// Arrange & Act
var workshop = new Workshop(1, "C# Basics", DateTime.Now, 20, "Paris", level, duration);
var result = workshop.GetPrice();
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData(true, 180)] // 150 + 30 lunch
[InlineData(false, 150)] // 150 base
public void Conference_GetPrice_IncludesLunch(bool includesLunch, decimal expected)
{
// Arrange & Act
var conf = new Conference(1, ".NET Conf", DateTime.Now, 100, "Lyon", includesLunch);
var result = conf.GetPrice();
// Assert
Assert.Equal(expected, result);
}
✍️ Tests à implémenter vous-même (4 tests)
En vous inspirant des exemples de polymorphisme ci-dessus, implémentez ces 4 tests :
Test 3 : Event.Register() - Inscription réussie
[Fact]
public void Event_Register_AddsParticipant()
{
// TODO: Tester qu'un participant s'inscrit correctement et incrémente AttendeesCount
}
Test 4 : Meetup.GetPrice() - Toujours gratuit
[Fact]
public void Meetup_GetPrice_AlwaysFree()
{
// TODO: Vérifier que GetPrice() retourne 0m pour un Meetup
}
Test 5 : Event.Cancel() - Annulation réussie
[Fact]
public void Event_Cancel_RemovesParticipant()
{
// TODO: Tester l'annulation d'inscription et la décrémentation d'AttendeesCount
}
Test 6 : Polymorphisme - Liste mixte d'événements
[Fact]
public void EventList_MixedTypes_PolymorphismWorks()
{
// TODO: Créer une List<Event> mixte et vérifier que GetPrice() renvoie les bonnes valeurs
}
🎓 Bonnes pratiques POO appliquées
Encapsulation - Protéger l'état interne
// ❌ MAUVAIS : Liste publique, pas de contrôle
public class Event
{
public List<Participant> Attendees = new List<Participant>(); // Dangereux !
}
// Problème : n'importe qui peut faire Attendees.Clear() ou ajouter sans validation
// ✅ BON : Liste privée, accès contrôlé
public class Event
{
private List<Participant> _attendees = new List<Participant>();
public int AttendeesCount => _attendees.Count; // Read-only
public bool Register(Participant p)
{
if (IsFull()) return false;
_attendees.Add(p);
return true;
}
public List<Participant> GetAttendees()
{
return new List<Participant>(_attendees); // Copie, pas l'original
}
}
Héritage - Réutiliser sans dupliquer
// ❌ MAUVAIS : Duplication du code commun
public class Workshop
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
private List<Participant> _attendees = new List<Participant>();
// ... tout dupliqué dans Conference et Meetup
}
// ✅ BON : Code commun dans la classe de base
public abstract class Event
{
// Propriétés et méthodes communes
public int Id { get; set; }
// ...
protected Event(...) { } // Constructeur commun
}
public class Workshop : Event
{
// Uniquement ce qui est spécifique au Workshop
public string Level { get; set; }
public Workshop(...) : base(...) { } // Appel du parent
}
Polymorphisme - Traiter uniformément
// ❌ MAUVAIS : if/switch sur le type
decimal CalculateTotalRevenue(List<Event> events)
{
decimal total = 0;
foreach (var evt in events)
{
if (evt is Workshop w)
total += w.Duration * 25m;
else if (evt is Conference c)
total += 150m;
else if (evt is Meetup)
total += 0m;
}
return total;
}
// ✅ BON : Méthode polymorphe
decimal CalculateTotalRevenue(List<Event> events)
{
return events.Sum(evt => evt.GetPrice() * evt.AttendeesCount);
// Appelle automatiquement la bonne implémentation
}
Interface vs Classe abstraite
Utilisez une interface quand :
- Contrat de comportement (IBookable, IPayable, IComparable)
- Implémentation multiple nécessaire
- Pas d'état commun à partager
Utilisez une classe abstraite quand :
- Code commun à réutiliser
- État partagé (fields, properties)
- Héritage unique suffit
// Interface : contrat pur
public interface IBookable
{
bool Register(Participant p);
bool IsFull();
}
// Classe abstraite : code + état partagé
public abstract class Event : IBookable
{
private List<Participant> _attendees; // État partagé
public abstract decimal GetPrice(); // Comportement variable
public virtual bool Register(Participant p) // Implémentation par défaut
{
// Code réutilisable par tous les enfants
}
}
💡 Indices progressifs
🔍 Indice 1 : Démarrer avec Event abstrait
public abstract class Event : IBookable
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public int Capacity { get; set; }
public string Location { get; set; }
private List<Participant> _attendees = new List<Participant>();
public int AttendeesCount => _attendees.Count;
protected Event(int id, string title, DateTime date, int capacity, string location)
{
Id = id;
Title = title;
Date = date;
Capacity = capacity;
Location = location;
}
// À implémenter : GetPrice(), Register(), Cancel(), IsFull(), GetAvailableSeats()
}
🔍 Indice 2 : Implémenter Workshop avec GetPrice()
public class Workshop : Event
{
public string Level { get; set; }
public int Duration { get; set; }
public Workshop(int id, string title, DateTime date, int capacity,
string location, string level, int duration)
: base(id, title, date, capacity, location)
{
Level = level;
Duration = duration;
}
public override decimal GetPrice()
{
decimal basePrice = Duration * 25m;
return Level switch
{
"Beginner" => basePrice * 0.8m,
"Intermediate" => basePrice,
"Advanced" => basePrice * 1.5m,
_ => basePrice
};
}
}
🔍 Indice 3 : Meetup avec surbooking
public class Meetup : Event
{
public Meetup(int id, string title, DateTime date, int capacity, string location)
: base(id, title, date, capacity, location)
{
}
public override decimal GetPrice() => 0m; // Gratuit
// Redéfinition pour autoriser 110% de capacité
public override bool Register(Participant participant)
{
int maxCapacity = (int)(Capacity * 1.1m);
if (AttendeesCount >= maxCapacity)
return false;
return base.Register(participant); // Appel du comportement parent
}
}
🔍 Indice 4 : Tester le polymorphisme
[Fact]
public void PolymorphismTest()
{
// Liste polymorphe : différents types, même interface
List<Event> events = new List<Event>
{
new Workshop(1, "W1", DateTime.Now, 20, "Paris", "Beginner", 4),
new Conference(2, "C1", DateTime.Now, 50, "Lyon", true),
new Meetup(3, "M1", DateTime.Now, 30, "Remote")
};
// Traitement uniforme
foreach (var evt in events)
{
evt.DisplayInfo(); // Polymorphisme : appelle la bonne version
Console.WriteLine($"Prix : {evt.GetPrice()}€");
}
}
📖 Ressources pour les tests
💪 Exercices bonus (optionnel)
Extensions du système :
- Ajouter type
OnlineWorkshop: hérite de Workshop + propriété URL Zoom - Implémenter
Speaker: Name, Bio, Topics, association avec Event - Créer
Venue: Name, Address, MaxCapacity, Facilities - Ajouter validation : Date ne peut pas être passée, Title non vide
- Property
IsUpcomingcalculée : Date > DateTime.Now
🎯 Questions d'entretien type
1. Différence entre classe abstraite et interface ?
Voir la réponse
Classe abstraite = code partagé + état (fields). Interface = contrat pur.
Event est abstraite (état _attendees partagé), IBookable est interface (contrat).
// Classe abstraite : code + état partagés
public abstract class Event
{
protected List<Participant> _attendees; // État partagé
public virtual bool Register() { } // Implémentation partagée
}
// Interface : contrat pur (pas d'état)
public interface IBookable
{
bool Register(Participant p); // Signature uniquement
bool Cancel(string email);
}
2. Pourquoi GetPrice() est abstraite dans Event ?
Voir la réponse
Chaque type d'événement calcule son prix différemment (Workshop selon niveau, Conference fixe, Meetup gratuit). Impossible d'avoir une implémentation par défaut commune.
public abstract class Event
{
public abstract decimal GetPrice(); // Pas d'implémentation par défaut
}
public class Workshop : Event
{
public override decimal GetPrice() => Level == "Advanced" ? 500 : 200;
}
public class Meetup : Event
{
public override decimal GetPrice() => 0; // Gratuit
}
3. Expliquer l'encapsulation avec _attendees
Voir la réponse
Liste privée, accès uniquement via Register(), Cancel(), GetAttendees(). Empêche modification directe (pas de attendees.Clear()). GetAttendees() retourne copie pour préserver encapsulation.
public abstract class Event
{
private List<Participant> _attendees = new(); // Privé = protégé
public virtual bool Register(Participant p)
{
if (IsFull()) return false;
_attendees.Add(p); // Accès contrôlé
return true;
}
public List<Participant> GetAttendees()
{
return new List<Participant>(_attendees); // Copie défensive
}
}
// ❌ Impossible de faire :
// event._attendees.Clear(); // Erreur compilation
4. Qu'est-ce que le polymorphisme dans votre code ?
Voir la réponse
List<Event> peut contenir Workshop, Conference, Meetup. foreach(var e in events) e.GetPrice() appelle automatiquement la bonne méthode selon le type réel.
List<Event> events = new()
{
new Workshop("C#", 100, "Advanced"), // GetPrice() → 500
new Conference("DevConf", 200, 150), // GetPrice() → 150
new Meetup("Tech Meetup", 50) // GetPrice() → 0
};
foreach (var e in events)
{
Console.WriteLine(e.GetPrice()); // Polymorphisme !
}
// Affiche : 500, 150, 0
5. Principe Open/Closed appliqué ?
Voir la réponse
Ajouter OnlineWorkshop = créer nouvelle classe qui hérite. Pas besoin de modifier Event, EventManager, ni GetTotalRevenue(). Code fermé à modification, ouvert à extension.
// ✅ Extension : Ajouter un nouveau type
public class OnlineWorkshop : Workshop
{
public string Platform { get; set; }
public override decimal GetPrice() => base.GetPrice() * 0.8m; // -20%
}
// ✅ Aucune modification du code existant
var manager = new EventManager();
manager.AddEvent(new OnlineWorkshop(...)); // Fonctionne !
manager.GetTotalRevenue(); // Fonctionne !
6. Pourquoi Register() est virtual, pas abstract ?
Voir la réponse
Implémentation par défaut convient pour Workshop/Conference. Seul Meetup redéfinit (surbooking). virtual permet réutilisation + spécialisation optionnelle.
public abstract class Event
{
// virtual = implémentation par défaut réutilisable
public virtual bool Register(Participant p)
{
if (IsFull()) return false; // Comportement standard
_attendees.Add(p);
return true;
}
}
public class Meetup : Event
{
// override optionnel pour comportement spécial
public override bool Register(Participant p)
{
if (_attendees.Count >= MaxCapacity * 1.2m) return false; // Surbooking
_attendees.Add(p);
return true;
}
}
public class Workshop : Event
{
// Pas de override = utilise implémentation par défaut
}
✅ Validation du module
Checklist apprenant :
- Exercice fil rouge : TechEvent Manager Phase 2 fonctionne
- Classes : Event abstraite + 3 types concrets implémentés
- Interface IBookable implémentée correctement
- Encapsulation :
_attendeesprivée, accès contrôlé - Polymorphisme : Liste mixte d'événements traités uniformément
- Tests : 6 tests essentiels passent, coverage ≥ 70%
- SOLID : Je peux expliquer les 5 principes appliqués
- Questions entretien : Je réponds aux 6 questions techniques
⬅️ Module 1 - Fondamentaux | Retour au MECA | Module 3 - Collections et LINQ ➡️