Ce terme recouvre des pratiques propres à la POO mais sa mise en place varie selon l’utilisation ou non de frameworks. Nous allons aborder l’essentiel dans cette page qui lui est consacrée. La partie sur JPA, DTO et Spring Boot sera abordée ultérieurement même si elle figure dans ce cours.
Le but de l’Encapsulation est de cacher le fonctionnement interne d’une classe tout en exposant uniquement les interfaces nécessaires au monde extérieur… mais pas seulement, il faut aussi vérifier les données initialisées.
Commençons par un exemple simple de l’utilisation d’une méthode setSolde(). Même si les setters sont indispensables pour les entités (notamment pour les frameworks comme JPA/Hibernate que nous étudierons plus tard), l’implémentation ci-dessous annule l’encapsulation car elle ne valide pas les données.
setSolde()
Voici une explication détaillée avec des propositions de solutions pour conserver les avantages des setters.
public void setSolde(BigDecimal solde) { this.solde = solde; // aucune validation ! Je peux saisir ce que je veux comme solde }
Quels sont les risques de notre méthode setSolde() ci-dessus ?
setSolde(new BigDecimal("-1000"))
NullPointerException
solde.add()
setSolde(null);
Voici une version sécurisée du setter, qui respecte l’encapsulation et valide les données :
public void setSolde(BigDecimal solde) { // On vérifie que solde n'est pas null if (solde == null) { throw new IllegalArgumentException("Le solde ne peut pas être null."); } // On vérifie que solde n'est pas négatif if (solde.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Le solde ne peut pas être négatif."); } this.solde = solde; }
Dans cette version, j’utilise la classe BigDecimal mais on peut aussi l’écrire avec un Double ou un Float, voire même avec des types primitifs double et float. Le problème restera le même, seule la syntaxe change.
BigDecimal
Double
Float
double
float
public void setSolde(Double solde) { // vérif solde pas null if (solde == null) { throw new IllegalArgumentException("Le solde ne peut pas être null."); } // vérif solde pas négatif if (solde < 0) { throw new IllegalArgumentException("Le solde ne peut pas être négatif."); } // si tout est ok, on initialise this.solde = solde; }
Avantages des versions (ci-dessus) :
Si on utilise JPA/Hibernate, les setters sont indispensables pour que le framework puisse hydrater les objets (lors d’une requête SQL). Nous verrons cela à partir de la semaine 5a.
JPA/Hibernate
hydrater les objets
Voici comment les implémenter correctement :
import javax.persistence.*; import java.math.BigDecimal; @Entity @Table(name = "comptes") public class CompteBancaire { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "solde", nullable = false) private BigDecimal solde; // Constructeur par défaut (obligatoire pour JPA) public CompteBancaire() {} // Getter public BigDecimal getSolde() { return solde; } // Notre setter sécurisé public void setSolde(BigDecimal solde) { if (solde == null) { throw new IllegalArgumentException("Le solde ne peut pas être null."); } if (solde.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Le solde ne peut pas être négatif."); } this.solde = solde; } // autres getters/setters... }
désérialisation
Si on veut éviter les setters tout en gardant la compatibilité avec JPA, voici quelques alternatives :
Remplacer les setters par des méthodes qui encapsulent la logique métier :
public void crediter(BigDecimal montant) { if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Le montant doit être positif."); } this.solde = this.solde.add(montant); } public void debiter(BigDecimal montant) { if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Le montant doit être positif."); } if (this.solde.compareTo(montant) < 0) { throw new IllegalStateException("Solde insuffisant !"); } this.solde = this.solde.subtract(montant); }
Avantages :
Inconvénient : moins compatible avec JPA et Hibernate a besoin de setters pour mapper les colonnes. On peut contourner cela avec des access types ( @Access(AccessType.FIELD)).
@Access(AccessType.FIELD)
Si on veut supprimer les setters et utiliser des méthodes métiers, il faut configurer JPA pour accéder directement aux champs :
@Entity @Access(AccessType.FIELD) // JPA accède directement aux attributs public class CompteBancaire { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "solde", nullable = false) private BigDecimal solde; // Pas de setter pour solde ! On utilise des méthodes métiers public void crediter(BigDecimal montant) { /* ... */ } public void debiter(BigDecimal montant) { /* ... */ } // Getter (obligatoire pour JPA) public BigDecimal getSolde() { return solde; } }
Inconvénient : moins flexible si on doit mettre à jour le solde depuis un script ou un outil, on devra utiliser les méthodes métiers.
solde != null
solde >= 0
throw new IllegalArgumentException("Le solde ne peut pas être négatif.")
crediter()
debiter()
/** * Définit le solde du compte. * * @param solde Le nouveau solde (doit être non null et positif). * @throws IllegalArgumentException Si le solde est null ou négatif. */ public void setSolde(BigDecimal solde) { Objects.requireNonNull(solde, "Le solde ne peut pas être null."); if (solde.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Le solde ne peut pas être négatif."); } this.solde = solde; }
@Entity @Access(AccessType.FIELD) // JPA accède directement aux champs public class CompteBancaire { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "solde", nullable = false) private BigDecimal solde = BigDecimal.ZERO; // Initialisation par défaut // Pas de setter pour solde ! On utilise des méthodes métiers public void crediter(BigDecimal montant) { Objects.requireNonNull(montant, "Le montant ne peut pas être null."); if (montant.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Le montant doit être positif."); } this.solde = this.solde.add(montant); } public void debiter(BigDecimal montant) { Objects.requireNonNull(montant, "Le montant ne peut pas être null."); if (montant.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Le montant doit être positif."); } if (this.solde.compareTo(montant) < 0) { throw new IllegalStateException("Solde insuffisant."); } this.solde = this.solde.subtract(montant); } // Getter (obligatoire pour JPA) public BigDecimal getSolde() { return solde; } }
Les setters ne sont pas mauvais en soi, mais ils doivent toujours valider les entrées pour respecter l’encapsulation. Pour les entités JPA, les setters sont souvent indispensables, mais on peut les sécuriser avec des validations. Pour les objets métiers, il faut plutôt écrire des méthodes spécifiques (crediter(), debiter()) qui encapsulent la logique. Utiliser @Access(AccessType.FIELD) si vous voulez éviter les setters avec JPA.
Lien vers un approfondissement des notions abordées