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).
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; }
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; } }
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; } }
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; } }
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. }
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; } }
final
crediter()
debiter()
setSolde()
throw new IllegalArgumentException("Solde invalide.")
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 ?
@Access(AccessType.FIELD)
@PrePersist
@PreUpdate
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); } }
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.
@NotNull
@Positive
@Size
Voici une analyse détaillée pour savoir quand et comment combiner les deux approches.
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 :
@Min
@Email
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.
Bean Validation
solde >= 0
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) }
Pourquoi ?
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 :
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; } }
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 }
@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); } }
@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 public class ClientService { public void crediterCompte(CompteBancaire compte, BigDecimal montant) { compte.crediter(montant); // Utilise la méthode métier } }
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; } }
setNom()
@Valid
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.
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 :
IllegalArgumentException
RuntimeException
“Un argument passé à une méthode est invalide.”
Avantages de IllegalArgumentException
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.
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 :
throws Exception
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.
Voici un tableau récapitulatif des exceptions courantes et leurs cas d’usage :
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."); }
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; }
Pourquoi IllegalArgumentException semble “passe-partout” ?
IllegalArgumentException est souvent utilisée car :
throw new IllegalArgumentException("L'âge ne peut pas être négatif.")
NullPointerException
Objects.requireNonNull(obj, "L'objet ne peut pas être null.")
IllegalStateException
throw new IllegalStateException("Compte fermé.")
UnsupportedOperationException
throw new UnsupportedOperationException("Ajout non supporté.")
SoldeInsuffisantException
throw new SoldeInsuffisantException("Solde insuffisant.")
FileNotFoundException
ResourceNotFoundException
throw new ResourceNotFoundException("Client non trouvé.")
Objects.requireNonNull()
IOException
Exception
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); } }
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 ?
throw new IllegalStateException("Le compte est fermé.")
throw new Exception("Erreur")
throw new IllegalArgumentException(...)