Accueil › Les fondations › Chapitre 4
La programmation objet
On te dira qu'il y a « quatre piliers ». C'est vrai, mais ça n'aide personne. Voici plutôt le problème que la POO résout, puis les quatre outils qu'elle propose — dans l'ordre où ils deviennent utiles.
Le problème que ça résout
Imaginons un programme qui gère des commandes, écrit « à plat » : des variables d'un côté, des fonctions de l'autre.
// des données nues, sans protection
decimal total = 0;
string statut = "brouillon";
List<string> lignes = [];
void Ajouter(string l) { lignes.Add(l); }
void Valider() { statut = "valide"; }
// ...400 lignes plus loin, ailleurs dans le programme :
statut = "vlaide"; // faute de frappe, personne ne bronche
total = -50; // total négatif accepté
lignes.Clear(); // commande vidée après validationN'importe quel bout du programme peut mettre les données dans un état absurde. Et quand il y a deux commandes en même temps ? Il faut tout dupliquer.
public class Commande
{
private readonly List<string> _lignes = [];
public Statut Statut { get; private set; } = Statut.Brouillon;
public decimal Total => _lignes.Count * 10m;
public IReadOnlyList<string> Lignes => _lignes;
public void Ajouter(string ligne)
{
if (Statut != Statut.Brouillon)
throw new InvalidOperationException("Commande déjà validée");
_lignes.Add(ligne);
}
public void Valider()
{
if (_lignes.Count == 0)
throw new InvalidOperationException("Commande vide");
Statut = Statut.Validee;
}
}Les règles vivent avec les données. Un état invalide devient impossible à produire, et créer une deuxième commande est gratuit.
Sans objets, tu es une entreprise où tout le monde a accès à tous les dossiers et peut écrire n'importe où. Avec des objets, chaque service possède ses dossiers et expose un guichet : « demande-moi de valider, je vérifierai ». On ne fouille pas dans les tiroirs des autres.
Pilier 1 — Encapsulation : cacher pour protéger
Encapsuler = rendre l'intérieur inaccessible, et n'exposer que des opérations qui laissent l'objet dans un état correct.
private reste libre d'évoluer. Tout ce qui est public
devient une promesse.- Commence tout en
privateet n'ouvre que sur besoin réel. - N'expose jamais une collection modifiable.
public List<T> Lignes { get; }laisse n'importe qui appeler.Clear(). ExposeIReadOnlyList<T>et fournis une méthodeAjouter. - Un objet naît valide. Ce qui est obligatoire passe par le constructeur (ou
required), pas par un « n'oublie pas d'appeler Init() ».
Pilier 2 — Héritage : « est un »
L'héritage permet à une classe de reprendre tout ce qu'une autre possède, puis d'ajouter ou de modifier.
public class Animal
{
public string Nom { get; init; } = "";
public virtual string Crier() => "…"; // virtual = les filles PEUVENT redéfinir
public void Dormir() => Console.WriteLine($"{Nom} dort"); // commun, non redéfinissable
}
public class Chien : Animal // « Chien EST UN Animal »
{
public override string Crier() => "Wouf"; // override = je redéfinis
}
public class Chat : Animal
{
public override string Crier() => "Miaou";
public void Ronronner() => Console.WriteLine("prrr"); // propre au chat
}
var c = new Chien { Nom = "Rex" };
c.Dormir(); // hérité tel quel
Console.WriteLine(c.Crier()); // Wouf
Animal a = c; // ✅ un Chien peut être vu comme un Animal
// c.Ronronner(); // ❌ un Chien n'a pas cette méthode
new au lieu de override
public class Base { public virtual string Dire() => "base"; }
public class Fille { } // ...
public class Mauvais : Base { public new string Dire() => "fille"; } // masque, ne redéfinit pas
Base x = new Mauvais();
Console.WriteLine(x.Dire()); // "base" 😱 — pas ce que tu voulaisLe mot-clé new sur un membre masque celui du parent au lieu de le remplacer : le
comportement dépend alors du type de la variable, pas de l'objet. Le compilateur t'avertit (CS0108) si
tu l'omets. Réponse presque toujours correcte : override.
Pilier 3 — Polymorphisme : un ordre, plusieurs comportements
C'est le pilier qui change vraiment la façon d'écrire du code. Sans lui, tu testes le type. Avec lui, tu demandes.
foreach (var a in animaux)
{
if (a is Chien) Console.WriteLine("Wouf");
else if (a is Chat) Console.WriteLine("Miaou");
else if (a is Vache) Console.WriteLine("Meuh");
// ← chaque nouvel animal impose de
// retrouver et modifier ce bloc,
// et tous ses jumeaux ailleurs.
}foreach (var a in animaux)
Console.WriteLine(a.Crier());
// Ajouter une Vache : une nouvelle classe,
// et RIEN à modifier ici.
// (C'est le « O » de SOLID : ouvert à
// l'extension, fermé à la modification.)Tu entres dans une salle et tu dis un seul mot : « présentez-vous ». Le Français répond en français, l'Anglais en anglais, le muet écrit sur un papier. Tu n'as pas eu à savoir qui était dans la salle. C'est ça, le polymorphisme : le même message, des réponses différentes selon le destinataire.
Chaque objet porte un pointeur vers la table de méthodes de son type réel. Un appel
a.Crier() sur une variable déclarée Animal consulte cette table à l'exécution
(dispatch dynamique) : c'est pourquoi le comportement suit l'objet, pas la variable. Une méthode non
virtual, elle, est résolue à la compilation — d'où le piège de new vu plus haut.
Coût réel : un déréférencement, souvent éliminé par le JIT quand il devine le type unique
(devirtualization). N'évite jamais virtual « pour la performance » sans mesure.
Pilier 4 — Abstraction
L'abstraction consiste à décrire ce qu'on peut faire sans dire comment c'est fait. C'est le sujet du chapitre suivant, qui lui est entièrement consacré — c'est le concept le plus rentable de tout C#.
| Pilier | Question à laquelle il répond | Outil C# |
|---|---|---|
| Encapsulation | Qui a le droit de toucher à quoi ? | private, propriétés, readonly |
| Héritage | Comment réutiliser sans copier-coller ? | : Base, virtual, override |
| Polymorphisme | Comment traiter des choses différentes uniformément ? | virtual/override, interfaces |
| Abstraction | Comment dépendre d'une idée plutôt que d'un détail ? | interface, abstract |
Composition > héritage (la leçon la plus utile du chapitre)
L'héritage est séduisant et piège les débutants. Il crée le couplage le plus fort qui existe : la fille dépend de tout le parent, y compris de ses détails internes futurs.
// « une commande, c'est comme une liste »
public class Commande : List<Ligne>
{
public decimal Total => this.Sum(l => l.Prix);
}
var c = new Commande();
c.Clear(); // 😱 hérité de List : vide la commande
c.Insert(0, l); // 😱 contourne toutes tes règles
// Impossible d'empêcher : tu as tout ouvert.// « une commande CONTIENT des lignes »
public class Commande
{
private readonly List<Ligne> _lignes = [];
public IReadOnlyList<Ligne> Lignes => _lignes;
public decimal Total => _lignes.Sum(l => l.Prix);
public void Ajouter(Ligne l) { /* règles ici */ }
}
// Tu contrôles exactement la surface exposée.Dis la phrase à voix haute. « Un chien est un animal » ✅ héritage. « Une voiture est un moteur » ❌ — non, elle a un moteur : composition. En pratique, dans une application métier, tu écriras beaucoup de composition et très peu d'héritage. C'est normal et sain.
Tout hérite d'object
Chaque type en .NET descend de object et possède donc quatre membres, qu'il est utile de
connaître :
public class Produit
{
public string Nom { get; init; } = "";
public decimal Prix { get; init; }
// Affichage lisible dans les logs et le débogueur — très rentable
public override string ToString() => $"{Nom} ({Prix:C})";
}
var p = new Produit { Nom = "Clé USB", Prix = 12.5m };
Console.WriteLine(p); // Clé USB (12,50 €) ← ToString() appelé implicitement
Console.WriteLine(p.GetType().Name); // Produit
Console.WriteLine(p.Equals(p)); // True
Console.WriteLine(p.GetHashCode()); // un entier
Un record fournit ToString, Equals et
GetHashCode corrects gratuitement (chapitre 10).
SOLID, en français et sans dogme
S — Responsabilité unique
Une classe, une raison de changer. Si tu dois dire « et aussi » pour la décrire, découpe-la.
Symptôme : une classe de 1 200 lignes nommée Manager ou
Helper.
O — Ouvert / fermé
On doit pouvoir ajouter un comportement sans modifier le code existant.
Symptôme : à chaque nouveau cas, tu ajoutes un else if dans le même
fichier.
L — Substitution de Liskov
Une fille doit pouvoir remplacer son parent sans surprendre l'appelant.
Contre-exemple classique : Carre : Rectangle, où changer la largeur
change la hauteur.
I — Ségrégation des interfaces
Mieux vaut plusieurs petites interfaces qu'une géante.
Symptôme : des implémentations pleines de
throw new NotImplementedException().
D — Inversion des dépendances
Dépends d'une interface, pas d'une classe concrète. C'est le
fondement de l'injection de dépendances.
Le plus utile des cinq au quotidien.
⚖️ Et le bon sens
SOLID est un jeu de symptômes à surveiller, pas une religion. Une application de 200 lignes découpée en 40 interfaces est pire que le problème qu'elle prétend résoudre.
Vérifie que c'est passé
🎯 Quiz — 4 questions
1. Tu exposes public List<Ligne> Lignes { get; }. Quel est le problème ?
get; sans set; empêche de remplacer la liste,
pas de la modifier. Expose IReadOnlyList<Ligne> et garde le
List<T> en champ privé.2. « Un UtilisateurAdmin est un Utilisateur avec des droits en plus. » Héritage ?
3. Animal a = new Chien(); a.Crier(); — quelle version est appelée si Crier est
virtual dans Animal et override dans Chien ?
4. Quel est le meilleur signal qu'une classe viole la responsabilité unique ?
Fiches de révision
virtual / override / new ?virtual autorise, override remplace, new masque (presque toujours une erreur).if (x is Type) : chaque type sait répondre lui-même.- La POO met les règles avec les données, pour rendre les états invalides impossibles.
- Encapsuler = tout
privatepar défaut ; n'expose jamais une collection modifiable. - Polymorphisme = supprimer les tests de type ; chaque objet sait quoi faire.
- Composition plutôt qu'héritage : « a un » plutôt que « est un ».
- Redéfinis
ToString(): cadeau gratuit pour les logs et le débogueur.