Problématique
Imaginons que vous développez une application bancaire où vous devez valider les montants des transactions. Vous avez plusieurs méthodes qui traitent différents types de transactions (virements, retraits, dépôts), et chacune doit respecter certaines limites de montant.
L'une des solutions qui nous vient est la suivante : consiste à ajouter manuellement la vérification de ces limites dans chaque méthode :
import java.math.BigDecimal; /** * Service bancaire avec validation des limites intégrée directement dans les méthodes */ public class BankService { // Constantes pour les limites de transactions private static final double VIREMENT_MIN = 10.0; private static final double VIREMENT_MAX = 10000.0; private static final double RETRAIT_MIN = 20.0; private static final double RETRAIT_MAX = 5000.0; private static final double DEPOT_MAX = 20000.0; /** * Effectue un virement avec validation des limites */ public void effectuerVirement(String destinataire, BigDecimal montant) { // Validation des limites double valeurMontant = montant.doubleValue(); if (valeurMontant < VIREMENT_MIN || valeurMontant > VIREMENT_MAX) { throw new TransactionLimitException( "Le montant du virement doit être entre " + VIREMENT_MIN + "€ et " + VIREMENT_MAX + "€" + " [actuel: " + valeurMontant + "€]" ); } // Logique métier System.out.println("Virement de " + montant + "€ vers " + destinataire + " effectué avec succès"); } /** * Effectue un retrait avec validation des limites */ public void effectuerRetrait(String compte, BigDecimal montant) { // Validation des limites double valeurMontant = montant.doubleValue(); if (valeurMontant < RETRAIT_MIN || valeurMontant > RETRAIT_MAX) { throw new TransactionLimitException( "Le montant du retrait doit être entre " + RETRAIT_MIN + "€ et " + RETRAIT_MAX + "€" + " [actuel: " + valeurMontant + "€]" ); } // Logique métier System.out.println("Retrait de " + montant + "€ du compte " + compte + " effectué avec succès"); } /** * Effectue un dépôt avec validation des limites */ public void effectuerDepot(String compte, BigDecimal montant) { // Validation des limites double valeurMontant = montant.doubleValue(); if (valeurMontant > DEPOT_MAX) { throw new TransactionLimitException( "Le dépôt ne peut pas dépasser " + DEPOT_MAX + "€" + " [actuel: " + valeurMontant + "€]" ); } // Logique métier System.out.println("Dépôt de " + montant + "€ sur le compte " + compte + " effectué avec succès"); } } /** * Exception levée lorsqu'une limite de transaction est dépassée */ class TransactionLimitException extends RuntimeException { public TransactionLimitException(String message) { super(message); } }
- Duplication du code : Le code de validation est répété dans chaque méthode, ce qui viole le principe DRY (Don't Repeat Yourself).
- Mélange des préoccupations : La logique de validation est mélangée avec la logique métier, ce qui rend le code moins lisible et plus difficile à maintenir.
- Manque de modularité : Si les règles de validation changent, vous devez modifier chaque méthode individuellement.
- Difficulté d'extension : L'ajout d'une nouvelle méthode nécessite de répéter le code de validation.
- Moins de déclarativité : Les contraintes ne sont pas clairement identifiables au premier coup d'œil comme avec les annotations.
Solution avec une annotation personnalisée
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.math.BigDecimal; /** * Annotation personnalisée pour valider les montants des transactions */ @Retention(RetentionPolicy.RUNTIME) // L'annotation sera disponible à l'exécution via la réflexion @Target(ElementType.METHOD) // L'annotation peut être appliquée aux méthodes public @interface TransactionLimit { double min() default 0.0; // Montant minimum autorisé double max() default Double.MAX_VALUE; // Montant maximum autorisé String message() default "Montant de transaction invalide"; // Message d'erreur personnalisé }
@TransactionLimit
:
- Définit les limites minimum et maximum pour un montant de transaction
- Permet de personnaliser le message d'erreur
- L'annotation est appliquée au niveau des méthodes avec
@Target(ElementType.METHOD)
- L'annotation est disponible à l'exécution avec
@Retention(RetentionPolicy.RUNTIME)
/** * Exception levée lorsqu'une limite de transaction est dépassée */ class TransactionLimitException extends RuntimeException { public TransactionLimitException(String message) { super(message); } } /** * Aspect qui intercepte les appels aux méthodes annotées avec @TransactionLimit * Note: Cet exemple utilise une implémentation simplifiée sans framework AOP. * Dans un cas réel, vous utiliseriez probablement AspectJ ou Spring AOP. */ class TransactionLimitAspect { /** * Valide le montant d'une transaction avant l'exécution d'une méthode */ public static void validateTransaction(Method method, Object[] args) { if (method.isAnnotationPresent(TransactionLimit.class)) { TransactionLimit limit = method.getAnnotation(TransactionLimit.class); // Recherche du montant dans les arguments for (Object arg : args) { if (arg instanceof BigDecimal) { BigDecimal amount = (BigDecimal) arg; double amountValue = amount.doubleValue(); // Validation des limites if (amountValue < limit.min() || amountValue > limit.max()) { throw new TransactionLimitException( limit.message() + " [min: " + limit.min() + ", max: " + limit.max() + ", actuel: " + amountValue + "]" ); } } } } } }
- La classe
TransactionLimitAspect
contient la logique de validation - Elle utilise la réflexion Java pour examiner les méthodes annotées et leurs arguments
- Elle vérifie si les montants de transaction respectent les limites définies
BankService
sont annotées avec des limites différentes selon le type de transaction/** * Service bancaire avec des méthodes qui utilisent l'annotation TransactionLimit */ class BankService { @TransactionLimit(min = 10.0, max = 10000.0, message = "Le montant du virement doit être entre 10€ et 10 000€") public void effectuerVirement(String destinataire, BigDecimal montant) { System.out.println("Virement de " + montant + "€ vers " + destinataire + " effectué avec succès"); } @TransactionLimit(min = 20.0, max = 5000.0, message = "Le montant du retrait doit être entre 20€ et 5 000€") public void effectuerRetrait(String compte, BigDecimal montant) { System.out.println("Retrait de " + montant + "€ du compte " + compte + " effectué avec succès"); } @TransactionLimit(max = 20000.0, message = "Le dépôt ne peut pas dépasser 20 000€") public void effectuerDepot(String compte, BigDecimal montant) { System.out.println("Dépôt de " + montant + "€ sur le compte " + compte + " effectué avec succès"); } }
TransactionProxy
intercepte les appels de méthode pour effectuer la validation avant l'exécution/** * Proxy pour intercepter les appels de méthode (implémentation simplifiée) */ class TransactionProxy { private final Object target; public TransactionProxy(Object target) { this.target = target; } /** * Méthode générique pour intercepter un appel de méthode */ public Object invoke(String methodName, Class<?>[] paramTypes, Object[] args) throws Exception { Method method = target.getClass().getMethod(methodName, paramTypes); // Validation avant l'exécution TransactionLimitAspect.validateTransaction(method, args); // Exécution de la méthode return method.invoke(target, args); } }
/** * Démonstration de l'utilisation */ public class TransactionDemo { public static void main(String[] args) { BankService bankService = new BankService(); TransactionProxy proxy = new TransactionProxy(bankService); try { // Transaction valide proxy.invoke("effectuerVirement", new Class<?>[] {String.class, BigDecimal.class}, new Object[] {"John Doe", new BigDecimal("1000.00")} ); // Transaction invalide - montant trop élevé proxy.invoke("effectuerRetrait", new Class<?>[] {String.class, BigDecimal.class}, new Object[] {"123456789", new BigDecimal("6000.00")} ); } catch (Exception e) { System.err.println("Erreur: " + e.getMessage()); } } }
Avantages de cette approche
- Séparation des préoccupations : La logique de validation est séparée de la logique métier.
- Réutilisabilité : L'annotation peut être appliquée à n'importe quelle méthode qui traite des transactions.
- Maintenabilité : Les règles de validation sont centralisées et faciles à modifier.
- Lisibilité : Le code métier reste propre et focalisé sur sa fonction principale.
- Extensibilité : Le système peut être facilement étendu pour ajouter d'autres types de validations.
Cette solution applique le principe AOP (Programmation Orientée Aspect) pour séparer la validation (aspect transversal) de la logique métier, ce qui rend le code plus modulaire et plus facile à maintenir.
Un article intéressant à lire ... : Java Dynamic proxy mechanism and how Spring is using it