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.

Sans objets
// 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 validation

N'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.

Avec un objet
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.

L'image

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.

Commande - _lignes (private) - _statut (private) + Ajouter + Valider le reste du programme ✗ interdit ✓ autorisé Le vert est un contrat. Le rouge peut changer demain sans rien casser.
Tout ce qui est private reste libre d'évoluer. Tout ce qui est public devient une promesse.
Trois réflexes d'encapsulation
  1. Commence tout en private et n'ouvre que sur besoin réel.
  2. N'expose jamais une collection modifiable. public List<T> Lignes { get; } laisse n'importe qui appeler .Clear(). Expose IReadOnlyList<T> et fournis une méthode Ajouter.
  3. 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
Animal Nom · Crier() · Dormir() Chien override Crier() → Wouf Chat override Crier() → Miaou + Ronronner()
La flèche se lit « est un ». C# n'autorise qu'un seul parent (pas d'héritage multiple) — mais autant d'interfaces qu'on veut.
Piège : 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 voulais

Le 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.

Sans polymorphisme
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.
}
Avec polymorphisme
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.)
L'image

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.

Sous le capot : la table virtuelle

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#.

PilierQuestion à laquelle il répondOutil C#
EncapsulationQui a le droit de toucher à quoi ?private, propriétés, readonly
HéritageComment réutiliser sans copier-coller ?: Base, virtual, override
PolymorphismeComment traiter des choses différentes uniformément ?virtual/override, interfaces
AbstractionComment 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.

Héritage détourné
// « 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.
Composition
// « 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.
Le test « est un / a un »

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 ?

Le 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 ?

Un utilisateur peut être promu ou rétrogradé ; on ne change pas le type d'un objet existant. Modélise les droits comme des données (rôles, claims), pas comme une hiérarchie de classes.

3. Animal a = new Chien(); a.Crier(); — quelle version est appelée si Crier est virtual dans Animal et override dans Chien ?

C'est la définition même du polymorphisme. Le type de la variable détermine ce que tu peux appeler ; le type réel de l'objet détermine ce qui s'exécute.

4. Quel est le meilleur signal qu'une classe viole la responsabilité unique ?

Le critère n'est pas la taille mais les raisons de changer. Une classe modifiée à la fois quand la TVA change et quand le format d'export change fait deux métiers.

Fiches de révision

Encapsulation en une phrase ?
Cacher l'intérieur, n'exposer que des opérations qui préservent un état valide.
virtual / override / new ?
virtual autorise, override remplace, new masque (presque toujours une erreur).
Polymorphisme, à quoi ça sert ?
À supprimer les cascades de if (x is Type) : chaque type sait répondre lui-même.
Héritage ou composition ?
Test « est un » vs « a un ». Dans le doute : composition.
Combien de parents en C# ?
Une seule classe de base, mais autant d'interfaces que nécessaire.
Le plus utile de SOLID ?
Le D : dépendre d'abstractions. C'est ce que fait l'injection de dépendances.
À retenir