L'encapsulation¶
En programmation, l’encapsulation désigne le principe de regrouper des données brutes avec un ensemble de routines permettant de les lire ou de les manipuler.
Ce principe est souvent accompagné du masquage de ces données brutes afin de s’assurer que l’utilisateur ne contourne pas l’interface qui lui est destinée. L’ensemble se considère alors comme une boîte noire ayant un comportement et des propriétés spécifiés.
L’encapsulation est un pilier de la programmation orientée objet, où chaque classe définit des méthodes ou des propriétés pour interagir avec les données membres, mais ce principe peut se rencontrer dans d’autres styles de programmation (par exemple la programmation modulaire).
-
L'encapsulation permet de modifier les structures de données internes sans modifier l’interface de celle-ci et donc sans pénaliser les utilisateurs. Cette situation arrive fréquemment lorsque l’on veut améliorer l’efficacité (rapidité de traitement) d’une classe ou d’un module, il faut souvent modifier les structures de données interne en conséquence.
-
L'encapsulation permet d’ajouter aisément des règles de validation et des contraintes d’intégrité, par exemple limiter le domaine des valeurs qu’une variable peut prendre (validité) ou vérifier que cette valeur n’entre pas en conflit avec les valeurs d’autres variables (intégrité).
-
L'encapsulation évite l’anti-pattern plat de spaghetti qui ne permet pas de déterminer le qui, le quoi et le comment d’une modification de données. En effet, l'application systématique de l'encapsulation impose un couplage faible et empêche donc le couplage fort, par espace commun ou par contenu, responsable du plat de spaghetti.
-
Plat de lasagne à définir.
-
Finalement, l'encapsulation permet d’offrir une interface orientée services et responsabilités, c’est-à-dire, d’offrir aux utilisateurs (programmeurs, abstractionnistes et architectes) de la classe ou du module une interface indiquant clairement quels services sont offerts et quelles sont les responsabilités de cette classe ou module.
1) Définition rapide¶
- Encapsulation : principe OO qui regroupe les données et le comportement et cache les détails internes derrière une API claire.
- Objectif : protéger les invariants (règles métier), limiter les couplages, rendre le code plus maintenable et testable.
- Concrètement : on rend les champs
private, on expose uniquement ce qui est nécessaire via des méthodes publiques (getters, setters, opérations métier).
À ne pas confondre avec abstraction (modéliser le quoi plutôt que le comment). L’encapsulation est un mécanisme pour atteindre l’abstraction.
2) Règles d’or (TL;DR)¶
- Champs
privatepar défaut. - N’exposez pas mécaniquement getters/setters pour tous les champs. Exposez le comportement métier quand c’est plus expressif (ex.
deposer(),retirer()plutôt quesetSolde()). - Validez dans le constructeur et dans les setters (si l’objet est mutable).
- Pour les collections ou types mutables, ne pas fuir la représentation : retournez des copies défensives ou des vues non modifiables.
- Considérez l’immutabilité (pas de setters) quand c’est possible (plus simple et thread‑safe par conception).
3) Conventions de nommage (JavaBeans)¶
- Getter :
getNom(); Setter :setNom(...) - Booléen :
isActif()(getter),setActif(boolean)(setter). - Ces conventions facilitent l’intégration avec des frameworks (JavaBeans, Jackson, JPA/Hibernate, etc.).
4) Exemples guidés¶
4.1 Objet mutable avec validations¶
public class Personne {
private String nom;
private int age;
public Personne(String nom, int age) {
setNom(nom); // validation centralisée
setAge(age);
}
public String getNom() { return nom; }
public void setNom(String nom) {
if (nom == null || nom.isBlank()) {
throw new IllegalArgumentException("Le nom ne peut pas être vide.");
}
this.nom = nom.trim();
}
public int getAge() { return age; }
public void setAge(int age) {
if (age < 0 || age > 130) {
throw new IllegalArgumentException("Âge invalide.");
}
this.age = age;
}
}
Points clés :
- Les champs restent private.
- Les setters valident et normalisent l’entrée (trim).
- Le constructeur réutilise les setters pour éviter la duplication.
4.2 Préférer des méthodes métier aux setters génériques¶
public class CompteBancaire {
private final String iban;
private double solde; // >= 0 toujours
public CompteBancaire(String iban, double soldeInitial) {
if (iban == null || iban.isBlank()) throw new IllegalArgumentException("IBAN requis");
if (soldeInitial < 0) throw new IllegalArgumentException("Solde initial >= 0");
this.iban = iban;
this.solde = soldeInitial;
}
public String getIban() { return iban; }
public double getSolde() { return solde; }
// Pas de setSolde() — on protège l'invariant via des opérations dédiées :
public void deposer(double montant) {
if (montant <= 0) throw new IllegalArgumentException("Montant > 0");
solde += montant;
}
public void retirer(double montant) {
if (montant <= 0) throw new IllegalArgumentException("Montant > 0");
if (montant > solde) throw new IllegalStateException("Fonds insuffisants");
solde -= montant;
}
}
Idée : pas de setSolde, on exprime l’intention métier (deposer/retirer), ce qui préserve l’invariant.
4.3 Collections et copies défensives¶
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Groupe {
private final List<String> membres = new ArrayList<>();
public void ajouter(String nom) { membres.add(nom); }
// Mauvais : fuite de la représentation interne !
// public List<String> getMembres() { return membres; }
// Mieux : vue non modifiable
public List<String> getMembres() {
return Collections.unmodifiableList(membres);
}
// Alternative : copie défensive
public List<String> snapshotMembres() {
return new ArrayList<>(membres);
}
}
Raison : si on retourne la liste interne, l’appelant peut la modifier et briser les invariants.
Astuce moderne :
List.copyOf(membres)(Java 10+) retourne une copie non modifiable.
4.4 Immutabilité (pas de setters)¶
import java.time.LocalDate;
import java.util.List;
public final class Etudiant {
private final String nom;
private final LocalDate naissance; // type immuable
private final List<Integer> notes; // doit être copié/figé
public Etudiant(String nom, LocalDate naissance, List<Integer> notes) {
if (nom == null || nom.isBlank()) throw new IllegalArgumentException("Nom requis");
this.nom = nom;
this.naissance = naissance; // LocalDate est immuable
this.notes = List.copyOf(notes); // fige le contenu
}
public String getNom() { return nom; }
public LocalDate getNaissance() { return naissance; }
public List<Integer> getNotes() { return notes; } // déjà non modifiable
}
Avantages : plus simple à raisonner, thread‑safe par conception, pas d’état caché qui change.
4.5 DTO (objets de transport) vs Modèle métier¶
- DTO : souvent de simples conteneurs de données, avec getters/setters sans logique, utilisés pour sérialisation (JSON), couches REST, mapping.
- Modèle métier : protège les invariants ; expose comportements et validations.
- Dans la pratique : ne pas confondre les deux ; éviter d’exposer le modèle métier brut au web, utilisez un mapper (ex. MapStruct) entre DTO et domaine.
5) Anti‑patterns courants¶
- Champs
public: casse l’encapsulation. - Générer automatiquement getters/setters pour tout sans réfléchir à l’API.
- Setters avec effets de bord imprévisibles (I/O, threads…).
- Fuite de mutabilité (retour d’objets internes modifiables).
- Exposer
Datemutable (préférezjava.time.*, immuables) ou copiez défensivement.
6) Conseils pratiques¶
- Valider tôt (constructeur) ; normaliser (trim, formats).
- Minimalisme : exposez le strict nécessaire.
- Préférez
finalsur les champs qui n’ont pas à changer. - Fluent API : possible mais restez clair (
withX(...)pour produire une nouvelle instance immuable). - Thread‑safety : l’immutabilité simplifie ; sinon, documentez et synchronisez au besoin.