Formation Java / Spring Boot – Qualité de code et professionnalisation
À l’issue de cette journée, vous serez capable de :
Le Clean Code n’est pas une religion. C’est une stratégie économique.
Clean Code
Dans un projet réel, la plupart du temps est passé à :
Un code pas clean coûte finalement plus cher que le temps gagné en le codant trop vite.
Un.e développeur.euse lit beaucoup plus de code qu’il n’en écrit, sans compter l’utilisation de l’IA pour générer une partie du code.
Votre objectif : écrire du code qui se lit facilement comme une intention.
Quand une méthode fait 3 choses :
Le code illisible crée :
Clean Code réduit la complexité en la rendant visible et structurée.
public void doIt(BigDecimal x) { ... }
public void crediter(BigDecimal montant) { ... }
But :
BigDecimal s = compte.getSolde();
BigDecimal soldeActuel = compte.getSolde();
But : votre cerveau ne doit pas mémoriser des abréviations
Un commentaire qui répète le code est inutile !
// on incrémente i i++;
// Règle métier : les intérêts ne s'appliquent pas aux comptes clôturés
Beaucoup trop de if imbriqués génère une logique illisible.
if
if (compte != null) { if (montant != null) { if (montant.compareTo(BigDecimal.ZERO) > 0) { ... } } }
if (compte == null) throw new IllegalArgumentException("compte"); if (montant == null) throw new IllegalArgumentException("montant"); if (montant.compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("montant"); ...
Ici, nous pourrions aussi utiliser un switch case.
Une méthode qui modifie sans prévenir est dangereuse.
String
int
On va en traiter plusieurs dans le TP…
SOLID = 5 principes simples, mais qui changent tout quand on fait du Spring.
Une classe doit avoir une seule raison de changer.
Un CompteService qui :
CompteService
Trop de responsabilités !
Bon exemple :
VirementService
InteretService
CompteQueryService
Ouvert à l’extension, fermé à la modification.
Exemple : demain, un CompteJoint s’ajoute dans nos options de choix.
CompteJoint
if (type.equals("COURANT")) ... else if (type.equals("EPARGNE")) ... else if (type.equals("JOINT")) ...
Mais vous avez déjà découvert l’intérêt des Patterns.
Un enfant doit pouvoir remplacer son parent sans casser le programme.
Exemple : si Compte doit implémenter la méthode debiter() alors un enfant (une sous-classe) ne doit pas trahir la règle
Compte
debiter()
Mauvais : un CompteEpargne qui accepte un débit négatif ou qui fait n’importe quoi.
CompteEpargne
Des petites interfaces ciblées plutôt qu’une grosse interface fourre-tout.
Mauvais :
interface CompteOperations { void debiter(); void crediter(); void calculerInterets(); void exporterCsv(); }
Bon :
DebitAutorise
CalculInterets
Exportable
Il vaut mieux dépendre d’abstractions, plutôt que de classes concrètes.
private final CompteRepositoryMemoire repo = new CompteRepositoryMemoire();
private final CompteRepository repo; public CompteService(CompteRepository repo) { this.repo = repo; }
On vous donne un service avec trop de traitement :
@Service public class BankService { public void operation(String type, String numeroSrc, String numeroDst, BigDecimal montant) { if(type.equals("VIREMENT")) { // trouver src, débiter, trouver dst, créditer, logs, validation... } else if(type.equals("CREDIT")) { // ... } else if(type.equals("DEBIT")) { // ... } } }
Problèmes :
Créer une classe dédiée : MontantValidator
MontantValidator
Créer :
DebitService
CreditService
OperationService
Créer une stratégie (préfigure le pattern Strategy)
Les tests existants doivent passer sans modification fonctionnelle.
public interface OperationService { String type(); // "VIREMENT", "CREDIT", ... void executer(OperationRequest req); }
@Service public class VirementService implements OperationService { private final CompteService compteService; public VirementService(CompteService compteService) { this.compteService = compteService; } @Override public String type() { return "VIREMENT"; } @Override public void executer(OperationRequest req) { compteService.virer(req.numeroSource(), req.numeroCible(), req.montant()); } }
@Service public class OperationDispatcher { private final Map<String, OperationService> operations; public OperationDispatcher(List<OperationService> services) { this.operations = services.stream() .collect(Collectors.toMap(OperationService::type, s -> s)); } public void executer(OperationRequest req) { OperationService op = operations.get(req.type()); if (op == null) throw new IllegalArgumentException("Type inconnu: " + req.type()); op.executer(req); } }
Avant de committer, vérifier :
Vous avez :