cursus.

Cours 2 · EncapsulationLeçon 1 sur 2

Visibilité et accesseurs

5 h de lecture10 sections Version PDF

À la fin de cette leçon, vous saurez

private, public, protected ; getters et setters ; invariants de classe et validation ; pourquoi un attribut public est une erreur de conception.

Le chapitre 2 a écrit une règle dans une méthode : on ne dépose pas un montant négatif, on n'emprunte pas un livre déjà emprunté. C'était censé être le gain de l'objet — un seul endroit où faire respecter une règle.

Sauf que rien n'oblige à passer par là.

compte.deposer(-500);      /* refusé par la méthode */compte.solde = -500;       /* accepté, et personne n'a rien vu */

La deuxième ligne contourne toute la logique de la classe. Tant qu'elle est possible, l'invariant n'est pas une garantie mais une politesse. Ce chapitre la rend impossible.

Les quatre niveaux de visibilité

Un modificateur devant un attribut ou une méthode dit qui a le droit d'y toucher.

ModificateurVisible depuis
privatela classe elle-même, et rien d'autre
(aucun)le paquetage — visibilité par défaut en Java
protectedle paquetage, plus les classes filles (chapitre 5)
publicpartout

Deux remarques sur ce tableau.

La visibilité par défaut n'est pas public, contrairement à ce que beaucoup supposent : c'est la visibilité de paquetage, plus restrictive. Écrire un attribut sans modificateur n'est donc pas neutre, c'est un choix — généralement fait par inadvertance.

protected est plus permissif que la visibilité par défaut, et pas seulement « pour les filles » : il ouvre aussi au paquetage entier. C'est une nuance qui surprend, et une raison de ne pas l'employer par réflexe.

La règle de conduite tient en une phrase : partir de private, et n'ouvrir que ce qu'on peut justifier. Restreindre après coup casse le code des autres ; ouvrir après coup ne casse rien.

Accesseurs

public class Compte {    private double solde;     public double getSolde() { return this.solde; }     public void deposer(double montant) {        if (montant <= 0) throw new IllegalArgumentException("montant non positif");        this.solde += montant;    }}

Trois choses à observer, et la troisième est le vrai sujet du chapitre.

Un accesseur en lecturegetSolde — laisse consulter sans laisser modifier. C'est le cas le plus fréquent, et le plus inoffensif.

Il n'y a pas de setSolde, et c'est délibéré. Un solde ne se fixe pas, il se modifie par des opérations métier : deposer, retirer. Fournir un setSolde public rétablirait exactement le trou qu'on vient de boucher.

Un accesseur n'est pas obligatoire. L'erreur la plus répandue chez les débutants est de générer mécaniquement un getter et un setter pour chaque attribut — l'éditeur le propose en un clic. Le résultat est une classe dont tous les champs sont modifiables de l'extérieur, avec deux lignes de cérémonie en plus : l'encapsulation est perdue, et le code est plus long.

La question à se poser pour chaque attribut est donc : qui a besoin de le lire ? qui a besoin de le modifier, et sous quelles conditions ? Le plus souvent, la réponse à la seconde question est « personne, sinon la classe elle-même ».

L'invariant de classe

C'est la notion que le chapitre doit installer, et elle donne un critère objectif.

Un invariant de classe est une propriété qui doit être vraie de la fin du constructeur jusqu'à la fin de la vie de l'objet, et qu'aucune méthode publique ne doit pouvoir violer.

Compte :   solde ≥ 0   et   titulaire ≠ nullLivre  :   si emprunteur ≠ null alors disponible = falseDate   :   1 ≤ jour ≤ nombre de jours du mois

Deux devoirs en découlent, et ils se répartissent clairement.

Le constructeur l'établit. Un objet ne doit pas pouvoir naître dans un état invalide : c'est le rôle de la validation vue au chapitre 2. Un constructeur qui accepte n'importe quoi reporte le problème sur toutes les méthodes.

Chaque méthode publique le préserve. Elle peut le rompre temporairement à l'intérieur — le temps d'une suite d'affectations — mais il doit être rétabli quand elle rend la main.

L'invariant donne enfin la réponse à « faut-il un setter ? ». Un setter est acceptable s'il peut vérifier l'invariant :

public void setTitulaire(String titulaire) {    if (titulaire == null || titulaire.isBlank()) {        throw new IllegalArgumentException("titulaire vide");    }    this.titulaire = titulaire;}

Un setter qui se contente de this.x = x n'est pas un setter : c'est un attribut public écrit en trois lignes.

Quiz · vérifiez votre compréhension Sans réponse

Un étudiant déclare tous ses attributs private, puis génère un getter et un setter pour chacun. Qu'a-t-il gagné ?

Pourquoi un attribut public est une erreur de conception

Quatre raisons, indépendantes les unes des autres.

On ne peut plus rien vérifier. C'est le cas de l'introduction, et le plus visible.

On ne peut plus changer la représentation interne. Si Point expose x et y publics, tout le code utilisateur écrit p.x — et passer plus tard en coordonnées polaires devient impossible sans réécrire chaque appelant. Avec getX(), la conversion se fait dans la méthode et personne d'autre n'est touché. C'est le bénéfice le moins visible et le plus durable.

On ne peut rien ajouter au passage. Journaliser les modifications, mettre un cache à jour, notifier un observateur, verrouiller pour l'accès concurrent : tout cela suppose un point de passage. Un attribut public n'en a pas.

On perd la lecture seule. Un attribut public est lisible et modifiable, sans nuance. Un getter sans setter donne exactement ce que final ne peut pas toujours exprimer.

Une fuite plus subtile

Encapsuler les attributs ne suffit pas toujours, et c'est le piège avancé du chapitre.

public class Reservation {    private Date debut;    public Date getDebut() { return this.debut; }     /* fuite */}

L'attribut est private, le getter ne fait que lire — et pourtant l'invariant n'est pas protégé. getDebut() rend la référence vers l'objet Date interne, au sens du chapitre 2 : l'appelant peut donc appeler setTime dessus et modifier la réservation de l'extérieur, sans jamais passer par une méthode de Reservation.

Deux parades. Rendre une copiereturn new Date(this.debut.getTime()) — ou, bien mieux, employer un type immuable, comme LocalDate en Java moderne : un objet qu'on ne peut pas modifier ne peut pas fuir.

La règle générale : l'encapsulation porte sur ce que l'objet laisse faire, pas seulement sur ce qu'il laisse voir. Rendre une référence vers une structure interne modifiable — un tableau, une liste, une date mutable — revient à la rendre publique.

Et l'excès inverse

Une nuance, pour ne pas transformer la règle en rituel.

Une classe qui n'est **qu'**un ensemble de getters et de setters, sans aucun comportement, est un symptôme : la logique qui devrait lui appartenir a été écrite ailleurs, chez ses appelants. On parle alors de modèle anémique, et l'on retombe sur le procédural du chapitre 1 avec plus de cérémonie.

Le remède tient en une formule : « dire, ne pas demander ». Plutôt que

if (compte.getSolde() >= montant) { compte.setSolde(compte.getSolde() - montant); }

on écrit compte.retirer(montant), et la règle reste où elle doit être. Le premier code met la logique chez l'appelant — donc dupliquée partout où l'on retire — le second la met dans l'objet qui possède la donnée.

Quiz · vérifiez votre compréhension Sans réponse

Une classe Reservation a un attribut private Date debut et un getter qui rend cet objet. L'invariant est-il protégé ?

À vous

L'exercice suit le fil de bibliothèque du semestre, et procède par dégradations successives.

D'abord un Livre à attributs publics : vous cassez son invariant en deux lignes, sans que rien ne proteste. Puis vous encapsulez, et vous constatez que les mêmes deux lignes ne compilent plus — ici, ne s'exécutent plus.

Ensuite le piège de la référence qui fuit : la classe rend sa liste d'emprunteurs, et l'appelant la vide. Vous écrirez les deux parades.

Enfin la démonstration du « dire, ne pas demander » : la même opération écrite chez l'appelant puis dans l'objet, et ce qui se passe quand la règle change.

Exercice · JavaScript · à vous de jouer

Cassez un invariant par des attributs publics, encapsulez, puis réparez la fuite de référence.

En attente
// ── 1. Attributs publics : l'invariant ne tient à rien ────────────────────
class LivreOuvert {
  constructor(titre) {
    this.titre = titre;
    this.disponible = true;
    this.emprunteur = null;
  }
  // Invariant voulu : emprunteur non nul  <=>  disponible faux.
  emprunter(qui) {
    if (!this.disponible) return false;
    this.disponible = false;
    this.emprunteur = qui;
    return true;
  }
  invariantOk() {
    return (this.emprunteur === null) === this.disponible;
  }
  toString() {
    return this.titre + " [" + (this.disponible ? "libre" : "pris par " + this.emprunteur) + "]";
  }
}

// ── 2. Version encapsulée ─────────────────────────────────────────────────
class Livre {
  #titre; #disponible; #emprunteurs;      // # = champ réellement privé

  constructor(titre) {
    if (!titre) throw new Error("un livre doit avoir un titre");
    this.#titre = titre;
    this.#disponible = true;
    this.#emprunteurs = [];
  }
  get titre() { return this.#titre; }
  estDisponible() { return this.#disponible; }

  emprunter(qui) {
    // ← à écrire : refuser si déjà emprunté, sinon enregistrer l'emprunteur
    return false;
  }
  rendre() { this.#disponible = true; }

  // ← FUITE : ce getter rend la liste INTERNE, que l'appelant peut vider.
  getEmprunteurs() { return this.#emprunteurs; }
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Cassez l'invariant de LivreOuvert en DEUX lignes, sans appeler emprunter.
// 2. Écrivez emprunter() dans Livre, et vérifiez que la même attaque échoue.
// 3. Réparez la fuite de getEmprunteurs, de deux façons : copie défensive,
//    puis exposition d'une valeur immuable.
// 4. Écrivez la même opération « emprunter si disponible » chez l'appelant,
//    puis dans l'objet. Que se passe-t-il quand la règle change ?

const a = new LivreOuvert("Dune");
console.log("   " + a.toString() + "   invariant :", a.invariantOk());

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 3 · sur machine

L'encapsulation qui fuit

Vérifier qu'un champ privé peut être modifié de l'extérieur si l'accesseur est mal écrit — et découvrir que l'invariant se protège, pas se déclare.

3 h
Avant de commencer
  • Le TP 2
  • Les collections de base de Java
  1. 1. Casser l'invariant

    Rendez publics les champs de Document. Écrivez du code extérieur qui met une année à moins mille et une disponibilité incohérente. Comptez les lignes qu'il vous a fallu.

  2. 2. Fermer

    Repassez tout en privé, ajoutez des accesseurs, et vérifiez que le code précédent ne compile plus.

  3. 3. La fuite par l'accesseur

    Ajoutez à Mediatheque une liste privée de documents et un accesseur qui la renvoie. Depuis l'extérieur, videz la liste. Constatez que le privé n'a rien protégé.

  4. 4. Colmater, trois façons

    Corrigez en renvoyant une copie, puis une vue non modifiable, puis en ne renvoyant rien du tout et en exposant les opérations utiles. Comparez les trois.

  5. 5. La fuite par le constructeur

    Passez une liste au constructeur de Mediatheque et gardez-en la référence. Modifiez ensuite la liste d'origine depuis l'extérieur. Corrigez.

  6. 6. Les accesseurs inutiles

    Comptez, dans votre code, les accesseurs qui ne servent qu'à recopier un champ vers l'extérieur. Pour chacun, demandez-vous quelle question du domaine il répond.

  7. 7. Déplacer le comportement

    Trouvez une portion de code extérieur qui lit trois accesseurs pour décider quelque chose. Déplacez cette décision DANS la classe et supprimez les accesseurs devenus inutiles.

  8. 8. Le champ final

    Rendez immuables les champs qui ne changent jamais après construction. Vérifiez ce que le compilateur refuse désormais.

C'est réussi quand
  • Vous videz une liste « privée » depuis l'extérieur, en une ligne
  • Après correction, la même ligne ne compile plus ou n'a plus d'effet
  • Vous avez supprimé au moins deux accesseurs en déplaçant une décision

Ce que la suite en fait

Le chapitre 4 complète la boîte à outils de la classe : ce qui appartient à la classe plutôt qu'aux objets — les membres statiques —, et la possibilité de donner plusieurs formes à une même opération, avec la surcharge.

Le chapitre 5 fera réapparaître protected, et l'on comprendra alors pourquoi ce modificateur est plus délicat qu'il n'en a l'air : ouvrir un attribut à ses classes filles, c'est signer un contrat avec du code qui n'est pas encore écrit.

À retenir

Flashcards · 1 / 6Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 10 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.