Aller au contenu

Encapsulation (suite) : Approfondissement

Quelques conseils ppur vous aider

Voici une analyse détaillée des conseils et bonnes pratiques (Cette approche permet de bien comprendre les fondamentaux avant d’introduire les frameworks qui imposent certaines pratiques comme les getters/setters).

Utiliser des champs privés

Fournir des méthodes d’accès (getters/setters)

Logique de validation dans les setters

public void setAge(int age) {
    if (age < 0 || age > 120) {
        throw new IllegalArgumentException("L'âge doit être compris entre 0 et 120.");
    }
    this.age = age;
}

Classes immuables

public final class Client {
    private final String nom;
    private final String email;

    public Client(String nom, String email) {
        this.nom = nom;
        this.email = email;
    }

    // Getters (pas de setters)
    public String getNom() { return nom; }
    public String getEmail() { return email; }

    // Pas de setters ! L'objet ne peut pas être modifié après création.
}

Exemple d’une classe Personne :

public class Personne {
    private String nom;
    private int age;

    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }

    // Getters
    public String getNom() { return nom; }
    public int getAge() { return age; }

    // Setters avec validation
    public void setNom(String nom) {
        if (nom == null || nom.trim().isEmpty()) {
            throw new IllegalArgumentException("Le nom ne peut pas être vide.");
        }
        this.nom = nom;
    }

    public void setAge(int age) {
        if (age < 0 || age > 120) {
            throw new IllegalArgumentException("L'âge doit être valide.");
        }
        this.age = age;
    }
}

Validation et Encapsulation

Objectifs : Découvrir les exceptions pour gérer les erreurs.

public class CompteBancaire {
    private String numero;
    private BigDecimal solde;

    public CompteBancaire(String numero, BigDecimal soldeInitial) {
        setNumero(numero);  // Utiliser le setter pour valider
        setSolde(soldeInitial);
    }

    // Getters
    public String getNumero() { return numero; }
    public BigDecimal getSolde() { return solde; }

    // Setters avec validation
    public void setNumero(String numero) {
        if (numero == null || !numero.matches("[A-Z]{2}\\d{10}")) {
            throw new IllegalArgumentException("Numéro de compte invalide.");
        }
        this.numero = numero;
    }

    public void setSolde(BigDecimal solde) {
        if (solde == null || solde.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Le solde ne peut pas être négatif.");
        }
        this.solde = solde;
    }
}

Méthodes Métiers vs Setters

Objectifs : Comprendre que les setters génériques ne sont pas toujours la meilleure solution et apprendre à utiliser des méthodes métiers pour encapsuler la logique.

public class CompteBancaire {
    private BigDecimal solde;

    // Pas de setter pour solde ! On utilise des méthodes métiers
    public void crediter(BigDecimal montant) {
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Montant invalide.");
        }
        solde = solde.add(montant);
    }

    public void debiter(BigDecimal montant) {
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Montant invalide.");
        }
        if (solde.compareTo(montant) < 0) {
            throw new IllegalStateException("Solde insuffisant.");
        }
        solde = solde.subtract(montant);
    }

    public BigDecimal getSolde() { return solde; }
}

Immuabilité

Objectifs : Comprendre les avantages des objets immuables et apprendre à créer des classes immuables.

public final class Adresse {
    private final String rue;
    private final String ville;
    private final String codePostal;

    public Adresse(String rue, String ville, String codePostal) {
        this.rue = rue;
        this.ville = ville;
        this.codePostal = codePostal;
    }

    // Getters (pas de setters)
    public String getRue() { return rue; }
    public String getVille() { return ville; }
    public String getCodePostal() { return codePostal; }

    // Pas de setters ! L'objet ne peut pas être modifié après création.
}

Introduction aux Frameworks (JPA, etc.)

Objectifs : Comprendre pourquoi les frameworks comme JPA imposent des getters/setters et apprendre à combiner encapsulation et compatibilité avec les frameworks.

@Entity
public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nom;

    @Column(nullable = false)
    private String email;

    // Constructeur par défaut (obligatoire pour JPA)
    public Client() {}

    // Constructeur avec validation
    public Client(String nom, String email) {
        setNom(nom);  // Utilise le setter pour valider
        setEmail(email);
    }

    // Getters (obligatoires pour JPA)
    public Long getId() { return id; }
    public String getNom() { return nom; }
    public String getEmail() { return email; }

    // Setters avec validation (obligatoires pour JPA)
    public void setNom(String nom) {
        if (nom == null || nom.trim().isEmpty()) {
            throw new IllegalArgumentException("Le nom ne peut pas être vide.");
        }
        this.nom = nom;
    }

    public void setEmail(String email) {
        if (email == null || !email.matches(".+@.+\\..+")) {
            throw new IllegalArgumentException("Email invalide.");
        }
        this.email = email;
    }
}

Tableau Récapitulatif

Concept Pourquoi c’est important Exemple
Encapsulation Protéger les données et contrôler les modifications. Champs privés + getters/setters validés.
Validation Garantir la cohérence des données. Vérifier qu’un solde n’est pas négatif.
Immuabilité Simplifier la gestion des états et le multithreading. Classes avec champs final et sans setters.
Méthodes métiers Encapsuler la logique métier plutôt que d’utiliser des setters génériques. crediter() et debiter() au lieu de setSolde().
Constructeurs Initialiser les objets dans un état valide dès la création. Valider les paramètres dans le constructeur.
Exceptions Gérer les erreurs de manière explicite. throw new IllegalArgumentException("Solde invalide.").
Design des classes Choisir entre immuabilité et mutabilité en fonction des besoins. Utiliser des classes immuables pour les DTOs, mutables pour les entités JPA.

Transition vers JPA

Nous verrons que JPA impose des getters/setters pour mapper les colonnes de la base de données aux attributs de l’objet.

Comment concilier encapsulation et JPA ?

Exemple avec JPA et validation :

@Entity
public class CompteBancaire {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private BigDecimal solde;

    // Getters (obligatoires pour JPA)
    public BigDecimal getSolde() { return solde; }

    // Setter sécurisé (utilisé par JPA)
    public void setSolde(BigDecimal solde) {
        if (solde == null || solde.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Solde invalide.");
        }
        this.solde = solde;
    }

    // Méthodes métiers (recommandées pour la logique métier)
    public void crediter(BigDecimal montant) {
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Montant invalide.");
        }
        this.solde = this.solde.add(montant);
    }

    public void debiter(BigDecimal montant) {
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Montant invalide.");
        }
        if (this.solde.compareTo(montant) < 0) {
            throw new IllegalStateException("Solde insuffisant.");
        }
        this.solde = this.solde.subtract(montant);
    }
}

Spring Boot (Bean Validation ou Contrôles dans les Setters)

Avec Spring Boot et Bean Validation (annotations comme @NotNull, @Positive, @Size, …), on peut automatiser une partie des validations, mais cela ne rend pas les contrôles dans les setters obsolètes.

Voici une analyse détaillée pour savoir quand et comment combiner les deux approches.

Rôle de Bean Validation (JSR-380)

Bean Validation (via des annotations comme @NotNull, @Min, @Email) permet de déclarer des contraintes sur les champs d’une classe. Ces contraintes sont automatiquement validées par Spring Boot lors :

Exemple avec Bean Validation :

public class ClientDTO {
    @NotNull(message = "Le nom ne peut pas être null.")
    @Size(min = 2, max = 50, message = "Le nom doit faire entre 2 et 50 caractères.")
    private String nom;

    @NotNull
    @Email(message = "L'email doit être valide.")
    private String email;

    @NotNull
    @Min(value = 0, message = "L'âge ne peut pas être négatif.")
    private int age;

    // Getters et setters (sans validation manuelle)
}

Avantages :

Mais alors, pourquoi les contrôles dans les setters restent utiles ?

Même avec Bean Validation, les contrôles dans les setters restent pertinents pour plusieurs raisons.

Raison Explication Exemple
Validation côté objet Bean Validation agit surtout au niveau des requêtes HTTP ou des opérations JPA, mais pas lors des modifications directes de l’objet. Si tu modifies un objet en mémoire (hors requête HTTP), Bean Validation ne s’applique pas.
Cohérence de l’état Les setters garantissent que l’objet reste toujours valide, même en dehors du contexte Spring. Un setter peut vérifier que solde >= 0 à chaque modification.
Logique métier complexe Bean Validation ne couvre pas les règles métiers spécifiques. Un setter peut appliquer des règles métiers personnalisées.
Sécurité défensive Une double validation dans les setters ajoute une couche de sécurité. Évite les bugs si Bean Validation est désactivé ou contourné.
Compatibilité hors Spring Si ton code est utilisé hors de Spring, les annotations Bean Validation ne fonctionnent pas. Les setters assurent la validation partout.

Quand Utiliser les Deux Approches ?

Cas 1 : DTOs (Data Transfer Objects)

Pour les DTOs (utilisés dans les contrôleurs REST), Bean Validation suffit généralement :

public class ClientDTO {
    @NotNull
    @Size(min = 2, max = 50)
    private String nom;

    @NotNull
    @Email
    private String email;

    // Getters et setters SANS validation manuelle
    // (la validation est faite par Spring via @Valid)
}

Cas 2 : Entités JPA

Pour les entités JPA, combiner Bean Validation et des setters validés est une bonne pratique.

@Entity
public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @NotNull
    @Size(min = 2, max = 50)
    private String nom;

    @Column(nullable = false, unique = true)
    @NotNull
    @Email
    private String email;

    @Column(nullable = false)
    @NotNull
    @Min(0)
    private int age;

    // Getters (obligatoires pour JPA)
    public String getNom() { return nom; }
    public String getEmail() { return email; }
    public int getAge() { return age; }

    // Setters AVEC validation manuelle (double sécurité)
    public void setNom(String nom) {
        if (nom == null || nom.length() < 2 || nom.length() > 50) {
            throw new IllegalArgumentException("Nom invalide.");
        }
        this.nom = nom;
    }

    public void setEmail(String email) {
        if (email == null || !email.matches(".+@.+\\..+")) {
            throw new IllegalArgumentException("Email invalide.");
        }
        this.email = email;
    }

    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("L'âge ne peut pas être négatif.");
        }
        this.age = age;
    }
}

Bean Validation :

Setters validés :

Cas 3 : Objets Métiers (Domain Objects)

Pour les objets métiers (qui encapsulent une logique complexe), préfère des méthodes métiers plutôt que des setters.

public class CompteBancaire {
    private BigDecimal solde;

    // Pas de setter pour solde ! On utilise des méthodes métiers
    public void crediter(BigDecimal montant) {
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Montant invalide.");
        }
        this.solde = this.solde.add(montant);
    }

    public void debiter(BigDecimal montant) {
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Montant invalide.");
        }
        if (this.solde.compareTo(montant) < 0) {
            throw new IllegalStateException("Solde insuffisant.");
        }
        this.solde = this.solde.subtract(montant);
    }

    public BigDecimal getSolde() { return solde; }
}

Bonnes pratiques pour combiner les 2 approches

Contexte Bean Validation Setters Validés Méthodes Métiers
DTOs ✅ Oui (obligatoire) ❌ Non (inutile) ❌ Non
Entités JPA ✅ Oui (pour les opérations JPA) ✅ Oui (pour la cohérence hors Spring) ⚠️ Optionnel (si logique métier simple)
Objets métiers ❌ Non (peu utile) ❌ Non (à éviter) ✅ Oui (recommandé)

Exemple Complet avec Spring Boot

DTO avec Bean Validation

public class ClientDTO {
    @NotNull
    @Size(min = 2, max = 50)
    private String nom;

    @NotNull
    @Email
    private String email;

    @NotNull
    @Min(0)
    private int age;

    // Getters et setters sans validation manuelle
}

Contrôleur REST avec @Valid

@RestController
@RequestMapping("/api/clients")
public class ClientController {
    @PostMapping
    public ResponseEntity<Client> createClient(@Valid @RequestBody ClientDTO clientDTO) {
        // Bean Validation vérifie automatiquement clientDTO
        Client client = new Client();
        client.setNom(clientDTO.getNom());
        client.setEmail(clientDTO.getEmail());
        client.setAge(clientDTO.getAge());
        // Sauvegarde en base de données...
        return ResponseEntity.ok(client);
    }
}

Entité JPA avec Setters Validés

@Entity
public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @NotNull
    @Size(min = 2, max = 50)
    private String nom;

    // Getters et setters avec validation manuelle
    public void setNom(String nom) {
        if (nom == null || nom.length() < 2 || nom.length() > 50) {
            throw new IllegalArgumentException("Nom invalide.");
        }
        this.nom = nom;
    }
    // ...
}

Service avec Logique Métier

@Service
public class ClientService {
    public void crediterCompte(CompteBancaire compte, BigDecimal montant) {
        compte.crediter(montant);  // Utilise la méthode métier
    }
}

Quand peut-on se passer des setters validés ?

On peut supprimer les validations dans les setters dans les cas suivants :

Exemple d’entité immuable :

@Entity
public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @NotNull
    @Size(min = 2, max = 50)
    private final String nom;

    @Column(nullable = false, unique = true)
    @NotNull
    @Email
    private final String email;

    // Constructeur avec validation
    public Client(String nom, String email) {
        if (nom == null || nom.length() < 2 || nom.length() > 50) {
            throw new IllegalArgumentException("Nom invalide.");
        }
        if (email == null || !email.matches(".+@.+\\..+")) {
            throw new IllegalArgumentException("Email invalide.");
        }
        this.nom = nom;
        this.email = email;
    }

    // Getters (pas de setters)
    public String getNom() { return nom; }
    public String getEmail() { return email; }
}

Résumé

Bonnes Pratiques Exemple Quand l’appliquer
Utiliser Bean Validation pour les DTOs @NotNull, @Size, @Email sur les champs des DTOs. Toujours pour les objets reçus via des requêtes HTTP.
Valider les setters des entités JPA Vérifier les valeurs dans les setters (ex. : setNom()). Pour garantir la cohérence même hors de Spring.
Préférer les méthodes métiers crediter(), debiter() au lieu de setSolde(). Pour les objets métiers avec une logique complexe.
Combiner les deux approches Bean Validation + setters validés. Pour les entités JPA critiques (ex. : comptes bancaires).
Rendre les DTOs immuables Champs final + constructeur validé. Si les DTOs ne doivent pas être modifiés après création.

Erreurs courantes

Erreur Correction
Oublier @Valid dans les contrôleurs Toujours annoter les paramètres des contrôleurs avec @Valid.
Setters sans validation Toujours valider les entrées dans les setters (même avec Bean Validation).
Utiliser des setters pour tout Préférer des méthodes métiers pour les objets complexes.
Ignorer les exceptions Toujours lever des exceptions claires pour les entrées invalides.
Mélanger DTOs et entités Séparer les DTOs (pour les requêtes HTTP) des entités (pour la base de données).

Bean Validation est indispensable pour valider les DTOs et les requêtes HTTP, mais ne suffit pas pour garantir la cohérence des objets en mémoire. Les setters validés sont complémentaires à Bean Validation, ils assurent la cohérence même hors de Spring. Ils permettent d’ajouter des règles métiers spécifiques.

Exception : IllegalArgumentException

Vous avez remarqué que j’utilise souvent IllegalArgumentException dans les exemples. C’est une sous-classe de RuntimeException (donc une exception non vérifiée) qui signifie :

“Un argument passé à une méthode est invalide.”

Avantages de IllegalArgumentException

Erreur Correction
Oublier @Valid dans les contrôleurs Toujours annoter les paramètres des contrôleurs avec @Valid.
Setters sans validation Toujours valider les entrées dans les setters (même avec Bean Validation).
Utiliser des setters pour tout Préférer des méthodes métiers pour les objets complexes.
Ignorer les exceptions Toujours lever des exceptions claires pour les entrées invalides.
Mélanger DTOs et entités Séparer les DTOs (pour les requêtes HTTP) des entités (pour la base de données).

Exemple d’utilisation typique :

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("L'âge ne peut pas être négatif.");
    }
    this.age = age;
}

Ici, IllegalArgumentException est parfaite car le problème vient clairement de l’argument age.

Pourquoi éviter Exception ?

Exception est la classe mère de toutes les exceptions vérifiées (checked exceptions). L’utiliser directement est généralement une mauvaise pratique pour plusieurs raisons :

Problème Explication
Trop générique Ne donne aucune information sur la nature de l’erreur.
Exceptions vérifiées Oblige à déclarer throws Exception ou à capturer l’exception, ce qui alourdit le code.
Mauvaise pratique Violent le principe de granularité des exceptions (il faut utiliser des exceptions spécifiques).
Difficile à déboguer Sans message clair, il est difficile de comprendre la cause de l’erreur.

Exemple à éviter :

public void setAge(int age) {
    if (age < 0) {
        throw new Exception("Erreur");  // trop générique !
    }
    this.age = age;
}

Ici, Exception ne dit rien sur la nature de l’erreur. De plus, elle est vérifiée, ce qui force à gérer l’exception avec un try-catch ou throws.

Quand utiliser IllegalArgumentException ?

Voici un tableau récapitulatif des exceptions courantes et leurs cas d’usage :

Problème Explication
Trop générique Ne donne aucune information sur la nature de l’erreur.
Exceptions vérifiées Oblige à déclarer throws Exception ou à capturer l’exception, ce qui alourdit le code.
Mauvaise pratique Violent le principe de granularité des exceptions (il faut utiliser des exceptions spécifiques).
Difficile à déboguer Sans message clair, il est difficile de comprendre la cause de l’erreur.

Quand utiliser d’autres exceptions ?

public void debiter(double montant) {
    if (montant > solde) {
        throw new IllegalStateException("Solde insuffisant.");
    }
    solde -= montant;
}

Ici, le problème n’est pas l’argument montant, mais l’état du compte (solde insuffisant).

public void setNom(String nom) {
    this.nom = Objects.requireNonNull(nom, "Le nom ne peut pas être null.");
    // Équivalent à :
    // if (nom == null) throw new NullPointerException("Le nom ne peut pas être null.");
}
public void addElement(E element) {
    throw new UnsupportedOperationException("Cette liste est immuable.");
}

Exceptions métiers personnalisées

Pour des règles métiers spécifiques, crée des exceptions personnalisées (qui étendent RuntimeException).

Exemple :

public class SoldeInsuffisantException extends RuntimeException {
    public SoldeInsuffisantException(String message) {
        super(message);
    }
}

// Utilisation
public void debiter(double montant) {
    if (montant > solde) {
        throw new SoldeInsuffisantException("Solde insuffisant: " + solde);
    }
    solde -= montant;
}

Avantages :

Pourquoi IllegalArgumentException semble “passe-partout” ?

IllegalArgumentException est souvent utilisée car :

Cas Exception à utiliser Exemple
Argument invalide IllegalArgumentException throw new IllegalArgumentException("L'âge ne peut pas être négatif.")
Objet null NullPointerException Objects.requireNonNull(obj, "L'objet ne peut pas être null.")
État invalide IllegalStateException throw new IllegalStateException("Compte fermé.")
Opération non supportée UnsupportedOperationException throw new UnsupportedOperationException("Ajout non supporté.")
Erreur métiers spécifique Exception personnalisée (ex. : SoldeInsuffisantException) throw new SoldeInsuffisantException("Solde insuffisant.")
Ressource introuvable FileNotFoundException (vérifiée) ou ResourceNotFoundException (personnalisée) throw new ResourceNotFoundException("Client non trouvé.")

Critères pour choisir une exception

Critère Recommandation
Argument invalide IllegalArgumentException
Objet null NullPointerException (ou Objects.requireNonNull())
État invalide IllegalStateException
Opération non supportée UnsupportedOperationException
Erreur métiers Exception personnalisée (ex. : SoldeInsuffisantException)
Erreur système RuntimeException ou sous-classe (ex. : IOException pour les E/S).
Éviter Exception Jamais (trop générique).

Exemple Complet avec Différentes Exceptions

public class CompteBancaire {
    private double solde;
    private boolean estFermé;

    public void crediter(double montant) {
        if (montant <= 0) {
            throw new IllegalArgumentException("Le montant doit être positif.");
        }
        if (estFermé) {
            throw new IllegalStateException("Le compte est fermé.");
        }
        solde += montant;
    }

    public void debiter(double montant) {
        if (montant <= 0) {
            throw new IllegalArgumentException("Le montant doit être positif.");
        }
        if (estFermé) {
            throw new IllegalStateException("Le compte est fermé.");
        }
        if (montant > solde) {
            throw new SoldeInsuffisantException("Solde insuffisant.");
        }
        solde -= montant;
    }

    public void fermer() {
        if (estFermé) {
            throw new IllegalStateException("Le compte est déjà fermé.");
        }
        estFermé = true;
    }
}

// Exception personnalisée
class SoldeInsuffisantException extends RuntimeException {
    public SoldeInsuffisantException(String message) {
        super(message);
    }
}

Quand Utiliser des Exceptions Vérifiées (Exception) ?

Les exceptions vérifiées (comme Exception, IOException) sont rarement utilisées dans le code métier. Elles sont généralement réservées à :

Exemple d’utilisation légitime :

public void lireFichier(String chemin) throws FileNotFoundException {
    FileInputStream fis = new FileInputStream(chemin);  // Peut lever FileNotFoundException
    // ...
}

Pourquoi éviter dans le code métier ?

  1. Résumé final
Pratique Exemple
Utiliser IllegalArgumentException pour les arguments invalides. throw new IllegalArgumentException("L'âge ne peut pas être négatif.")
Utiliser IllegalStateException pour les états invalides. throw new IllegalStateException("Le compte est fermé.")
Créer des exceptions personnalisées pour les règles métiers. throw new SoldeInsuffisantException("Solde insuffisant.")
Éviter Exception (trop générique). throw new Exception("Erreur")
Utiliser NullPointerException pour les objets null. Objects.requireNonNull(obj, "L'objet ne peut pas être null.")
Préférer les exceptions non vérifiées (RuntimeException) pour les erreurs de programmation. throw new IllegalArgumentException(...)