Aller au contenu

Encapsulation

encapsulation

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.

Méthode setter

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.

Voici une explication détaillée avec des propositions de solutions pour conserver les avantages des setters.

Problèmes de l’implémentation actuelle

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 ?

Problèmes Explications Exemples de risque
Absence de validation Le solde peut être mis à n’importe quelle valeur (négatif, null,…). setSolde(new BigDecimal("-1000")) : Un solde négatif est illogique pour un compte bancaire.
Violation de l’encapsulation L’attribut solde est modifié sans contrôle, ce qui va à l’encontre du principe d’encapsulation. Un code externe peut corrompre l’état de l’objet (s’il possède sa référence) !
Risque d’avoir un NullPointerException Si solde est null, les opérations ultérieures (solde.add()) lèveront une exception. setSolde(null); entraine un NullPointerException lors d’un calcul.

Comment corriger notre setter ?

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.

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) :

Amélioration Explication
Validation des entrées Le solde doit être non null et positif.
Encapsulation respectée L’attribut solde ne peut être modifié que via ce setter contrôlé.
Messages d’erreur clairs Les exceptions indiquent pourquoi la valeur est invalide.
Robustesse Évite les NullPointerException et les états incohérents.

Exemple complet avec une entité JPA

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.

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

Pourquoi cette implémentation est correcte ?

Alternatives aux setters Classiques

Si on veut éviter les setters tout en gardant la compatibilité avec JPA, voici quelques alternatives :

Utiliser des méthodes métiers

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

Utiliser @Access(AccessType.FIELD) avec JPA

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;
    }
}

Avantages :

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.

Bonnes Pratiques pour les Setters

Pratique Exemple
Toujours valider les entrées Vérifier que solde != null et solde >= 0.
Utiliser des exceptions descriptives throw new IllegalArgumentException("Le solde ne peut pas être négatif.")
Préférer les méthodes métiers Remplacer setSolde() par crediter()/debiter() si possible.
Documenter les contraintes Ajouter des commentaires Javadoc pour expliquer les règles.
Utiliser @Access(AccessType.FIELD) Pour éviter les setters avec JPA (si vous utilisez des méthodes métiers).

Exemple avec Javadoc

/**
 * 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;
}

Quand Utiliser des Setters ?

Cas d’Usage Recommandation
Classes simples (DTO, entités JPA) Utiliser des setters validés pour la compatibilité avec les frameworks.
Objets métiers complexes Préférer des méthodes métiers (crediter()) pour encapsuler la logique.
Immuabilité Éviter les setters et utiliser des constructeurs ou des méthodes de fabrication (factory methods).

Exemple : Entité JPA avec Méthodes Métiers

@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;
    }
}

Conclusion

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