Leçon 3/8 10 min

L'encapsulation : la capsule

Cacher les données dans l'objet et n'exposer qu'une porte contrôlée. Pourquoi protéger son état change tout.

Le problème : la porte grande ouverte

À la leçon précédente, on fabriquait des comptes avec une méthode deposer() qui contient les règles. Sauf que rien n'empêche d'écrire, ailleurs dans le code :

compte.solde = 999999;   // on contourne deposer() et toutes ses règles
compte.solde = -500;     // un solde négatif ? pourquoi pas, personne ne vérifie

Si n'importe qui peut modifier le solde directement, tes garde-fous ne servent à rien. Le jour où un bout de code (le tien, ou celui d'un collègue) écrit une valeur incohérente, tu auras un bug impossible à retrouver : qui a mis ce solde à -500, et ? C'est exactement le genre de chaos que la POO veut éviter.

L'idée : cacher l'intérieur, n'exposer qu'une porte

L'encapsulation, c'est fermer l'objet : ses données deviennent privées (inaccessibles de l'extérieur), et la seule façon d'agir dessus est de passer par ses méthodes publiques, qui imposent les règles. L'objet devient une capsule : on voit les boutons, pas les rouages.

L'image juste, c'est le distributeur de boissons. Tu appuies sur un bouton (méthode publique), la machine te sort une canette. Tu n'ouvres pas la machine pour prendre une canette à la main, et tu ne touches pas à la caisse à l'intérieur (donnée privée). L'interface est réduite et contrôlée : c'est ce qui rend la machine fiable.

L'objet Compte protège sa donnée privée solde derrière un mur ; on ne peut y agir que par les méthodes publiques deposer et retirer. Un accès direct à solde est refusé. objet Compte privé solde = 120 méthodes publiques (les portes) deposer() retirer() compte.solde = 999999 compte .deposer(50) La donnée est protégée ; on n'agit dessus que par des portes qui imposent les règles.
La donnée privée est à l'abri ; les méthodes publiques sont les seules portes, et elles imposent les règles.

En code : private, public, et les accesseurs

On marque les données privées (souvent private, ou un # en JavaScript) et les méthodes utiles publiques. Pour lire une donnée privée sans permettre de l'écrire n'importe comment, on expose un accesseur (un « getter ») :

Prédisez avant de lire

Le solde d'un compte est déclaré public : n'importe quel code peut écrire compte.solde = -9999 et mettre l'objet dans un état impossible. Avant de dérouler : si solde est public, qu'est-ce qui empêche cet accès direct de corrompre l'invariant « pas de solde négatif » ? Et que change le fait de le rendre privé avec une méthode retirer() qui vérifie ?

Voir la réponse

Rien n'empêche l'écriture directe si solde est public : n'importe quel code peut lui affecter n'importe quelle valeur, l'objet ne peut pas se défendre. Rendre le champ privé (#solde en JS) le met hors de portée du dehors : écrire compte.#solde hors de la classe est une erreur de syntaxe, et un compte.solde = -9999 ne créerait qu'une propriété publique parasite, ignorée par les méthodes qui, elles, travaillent sur le vrai #solde. Au final, seules les méthodes de la classe modifient l'état, et elles peuvent y mettre toutes les vérifications utiles (montant positif, solde suffisant…). C'est l'encapsulation : la classe garde la maîtrise de son propre état et garantit ses invariants.

classe Compte:
    privé solde = 0          // inaccessible de l'extérieur

    public deposer(montant):
        si montant > 0:       // la règle vit ici, impossible à contourner
            this.solde += montant

    public retirer(montant):
        si montant > 0 et montant <= this.solde:
            this.solde -= montant

    public lireSolde():       // accesseur : on peut LIRE, pas écrire
        retourne this.solde

compte.solde = 999999        // ❌ refusé : solde est privé
compte.deposer(50)           // ✅ seule voie autorisée

Bénéfice immédiat : il n'existe qu'un seul endroit où le solde change (les méthodes). Un solde incohérent devient impossible, pas juste « pas censé arriver ». Et tu peux changer l'intérieur (stocker en centimes, ajouter un journal…) sans rien casser dehors, tant que les portes ne changent pas.

Le code ci-dessous utilise un vrai champ privé #solde en JavaScript. Clique sur Run et observe : le dépôt et le retrait passent par les méthodes, l'invariant tient. Essaie de décommenter la ligne // compte.#solde = 9999 : tu verras que le compilateur TypeScript la refuse d'emblée.

class Compte {
  #solde = 0;

  deposer(montant) {
    if (montant > 0) this.#solde += montant;
  }

  retirer(montant) {
    if (montant > 0 && montant <= this.#solde) {
      this.#solde -= montant;
    } else {
      console.log('Refus : montant invalide ou solde insuffisant.');
    }
  }

  lireSolde() { return this.#solde; }
}

const compte = new Compte();
compte.deposer(100);
compte.retirer(30);
console.log('Solde :', compte.lireSolde()); // 70
compte.retirer(200); // refus
console.log('Solde apres tentative impossible :', compte.lireSolde()); // 70
// compte.#solde = 9999; // SyntaxError : champ prive inaccessible de l'exterieur
console.log('Invariant garanti : le #solde ne peut etre ecrit que par les methodes.');

La capsule en vrai (essaie de tricher)

Ce Compte garde son solde en privé (un vrai champ privé JS #solde). Essaie la voie normale… puis essaie de tricher en écrivant le solde directement.

solde (lecture seule) 0 €

Ce que l'encapsulation t'apporte

  • Des invariants garantis : un solde négatif devient impossible, pas juste déconseillé.
  • Un seul endroit à débugger : si le solde est faux, le coupable est forcément dans une méthode de la classe.
  • La liberté de changer l'intérieur : tant que les méthodes publiques gardent la même signature, tu refais la cuisine interne sans casser le reste du programme.

Règle d'or : expose le minimum. Données privées par défaut, méthodes publiques seulement pour ce dont l'extérieur a vraiment besoin. Une petite surface publique = un objet facile à comprendre, à utiliser et à faire évoluer.

The problem: the wide-open door

In the previous lesson, we built accounts with a deposer() method holding the rules. Except nothing stops you from writing, elsewhere in the code:

account.solde = 999999;   // we bypass deposer() and all its rules
account.solde = -500;     // a negative balance? sure, nobody checks

If anyone can change the balance directly, your safeguards are useless. The day some code (yours, or a colleague's) writes an inconsistent value, you'll have a bug that's impossible to track: who set this balance to -500, and where? That's exactly the chaos OOP wants to avoid.

The idea: hide the inside, expose only a door

Encapsulation means closing the object: its data becomes private (unreachable from outside), and the only way to act on it is through its public methods, which enforce the rules. The object becomes a capsule: you see the buttons, not the gears.

The right image is the vending machine. You press a button (public method), the machine hands you a can. You don't open the machine to grab a can by hand, and you don't touch the cash inside (private data). The interface is small and controlled: that's what makes the machine reliable.

The Compte object protects its private balance behind a wall; you can only act on it through the public methods deposit and withdraw. A direct access to the balance is refused. Account object private balance = 120 public methods (the doors) deposit() withdraw() account.balance = 999999 account .deposit(50) The data is protected; you act on it only through doors that enforce the rules.
The private data is safe; the public methods are the only doors, and they enforce the rules.

In code: private, public, and accessors

We mark the data private (often private, or a # in JavaScript) and the useful methods public. To read a private value without allowing it to be written carelessly, we expose an accessor (a "getter"):

Predict before reading

An account's balance is declared public: any code can write account.balance = -9999 and push the object into an impossible state. Before you expand: if balance is public, what stops that direct write from breaking the "no negative balance" invariant? And what changes when you make it private with a withdraw() method that validates?

See the answer

Nothing stops the direct write if balance is public: any code can assign any value, the object has no way to defend itself. Making the field private (#balance in JS) puts it out of reach from the outside: writing account.#balance outside the class is a syntax error, and an account.balance = -9999 would only create a stray public property, ignored by the methods, which work on the real #balance. In the end, only the class's own methods change the state, and they can enforce any rules needed (positive amount, sufficient balance…). That is encapsulation: the class retains control over its own state and guarantees its invariants.

class Account:
    private balance = 0      // unreachable from outside

    public deposit(amount):
        if amount > 0:        // the rule lives here, impossible to bypass
            this.balance += amount

    public withdraw(amount):
        if amount > 0 and amount <= this.balance:
            this.balance -= amount

    public getBalance():      // accessor: you can READ, not write
        return this.balance

account.balance = 999999     // ❌ refused: balance is private
account.deposit(50)          // ✅ the only allowed way

Immediate benefit: there is a single place where the balance changes (the methods). An inconsistent balance becomes impossible, not just "shouldn't happen". And you can change the inside (store cents, add a log…) without breaking anything outside, as long as the doors stay the same.

The code below uses a real private field #balance in JavaScript. Click Run and observe: deposits and withdrawals go through the methods, the invariant holds. Try uncommenting the line // account.#balance = 9999: the TypeScript compiler will refuse it outright.

class Account {
  #balance = 0;

  deposit(amount) {
    if (amount > 0) this.#balance += amount;
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    } else {
      console.log('Refused: invalid amount or insufficient balance.');
    }
  }

  getBalance() { return this.#balance; }
}

const account = new Account();
account.deposit(100);
account.withdraw(30);
console.log('Balance:', account.getBalance()); // 70
account.withdraw(200); // refused
console.log('Balance after failed attempt:', account.getBalance()); // 70
// account.#balance = 9999; // SyntaxError: private field, inaccessible from outside
console.log('Invariant holds: #balance can only be written by the class methods.');

The capsule for real (try to cheat)

This Compte keeps its balance private (a real JS private field #solde). Try the normal way… then try to cheat by writing the balance directly.

solde (read-only) 0 €

What encapsulation gives you

  • Guaranteed invariants: a negative balance becomes impossible, not just discouraged.
  • A single place to debug: if the balance is wrong, the culprit is necessarily inside a method of the class.
  • Freedom to change the inside: as long as the public methods keep the same signature, you redo the internal plumbing without breaking the rest of the program.

Golden rule: expose the minimum. Data private by default, public methods only for what the outside truly needs. A small public surface = an object that's easy to understand, use and evolve.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

⚖️ Juge le code de l'IA
Accepter ou rejeter la conception de l'IA

Tu demandes à l'IA d'« encapsuler » la classe Compte. Elle te rend ceci. Ton rôle de relecteur : l'accepter telle quelle ou la rejeter, et dire pourquoi.

class Compte {
  #solde = 0;
  get solde(){ return this.#solde; }
  set solde(v){ this.#solde = v; }   // setter public, sans contrôle
}

compte.solde = -500;   // passe : le setter ne vérifie rien
Rejeter. Le champ #solde est bien privé, mais le set solde(v) rouvre la porte en grand : depuis l'extérieur, compte.solde = -500 passe sans aucun contrôle. C'est de la fausse encapsulation : on a juste déplacé l'accès direct dans un setter sans règle. Un getter en lecture seule, oui ; un setter public sans validation revient à rendre la donnée publique. Garde le get, supprime le set, et fais passer les écritures par deposer() / retirer() qui imposent les invariants.
🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : qu'est-ce que l'encapsulation, et pourquoi rendre solde privé protège-t-il mieux qu'un simple commentaire « ne pas toucher » ?

L'encapsulation cache les données d'un objet (elles deviennent privées) et n'autorise l'accès que par des méthodes publiques qui imposent les règles. Rendre solde privé est plus fort qu'un commentaire parce que c'est garanti par le langage : compte.solde = -500 est refusé, pas juste déconseillé. Résultat : un seul endroit où le solde change, donc un invariant garanti et un seul endroit à débugger.
🔧 Débugue le code

Symptôme : Le solde d'un compte ne devrait JAMAIS pouvoir descendre sous 0. Pourtant ce code affiche Solde : -500 : n'importe quel code extérieur écrit directement dans solde et le met dans un état impossible. Répare la classe pour que l'état soit protégé.

class Compte {
  constructor() {
    this.solde = 0;
  }
}

const c = new Compte();
c.solde = -500;   // personne ne vérifie : état impossible
console.log("Solde :", c.solde);
La cause : solde est une propriété publique, modifiable depuis l'extérieur sans aucune règle. La correction : rendre le champ PRIVÉ avec #solde, et n'exposer que des méthodes qui imposent les règles.
À quoi sert l'encapsulation ?
Pourquoi rendre une donnée privée plutôt que publique ?
Que signifie déclarer une propriété private dans une classe ?
Un compte a un champ #solde privé et une méthode retirer(montant) qui refuse si le solde est insuffisant. Pourquoi est-ce mieux qu'un champ solde public ?
Prochaine étape

Tes objets sont solides et bien fermés. Maintenant, comment créer un CompteÉpargne qui fait presque comme un Compte, mais avec un taux d'intérêt en plus, sans tout réécrire ? C'est le rôle de l'héritage.

Leçon 4 : L'héritage →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement