samedi 27 avril 2024

[Java ] Les interfaces fonctionnelles Java pour inverser les dépendances

Avez-vous déjà envisagé d’utiliser les interfaces fonctionnelles Java pour inverser les dépendances dans vos projets Java ?
Dans cet article, nous allons explorer comment nous pouvons le faire en utilisant trois interfaces clés : Supplier, Consumer et Function.

Supplier
L’interface Supplier est utilisée lorsque vous devez fournir un objet sans nécessiter de paramètres d’entrée.

Voici l’interface Supplier :

public interface Supplier<T>{
   T get();
}

Pour mieux comprendre la nécessité d’utiliser cette interface, jetons un coup d’œil à un peu de code :

public class Logger{
   public void log(String message){
      if(isLogEnabled()){
        write(message);
      }
   }
}

// Utilisation de la classe Logger
public class Controller{
   @Inject 
    Logger logger;

   public void execute(){
      logger.log(generateLogMessage());
   }
}

Dans le code ci-dessus, nous avons une classe Logger responsable de l’écriture des messages de log si la journalisation est activée.
La classe Controller invoque le logger en passant le résultat de la méthode generateLogMessage. Jusqu’à présent, tout semble bien.
Cependant, imaginez si la méthode generateLogMessage implique un traitement lourd ou consomme des ressources significatives, et que la journalisation est désactivée.
Dans de tels cas, des ressources précieuses sont gaspillées car le message de log produit ne sera pas utilisé.

La solution à ce problème est de passer un Supplier à la classe Logger qui renverra le message lorsqu’il est demandé, et le logger appelle simplement la méthode dans le cas où le log est activé, comme suit :

public class Logger{
   public void log(Supplier<String> messageSupplier){
      if(isLogEnabled()){
        write(messageSupplier.get());
      }
   }
}

// Utilisation de la classe Logger
public class Controller{
   @Inject Logger logger;

   public void execute(){
      logger.log(() -> generateLogMessage());
   }
}

Maintenant, la méthode generateLogMessage ne sera exécutée que dans le cas où la méthode get du fournisseur est appelée, puis nous économisons des ressources dans le cas où le log n’est pas activé. De plus, avec ce type de solution utilisant le Supplier, nous avons la flexibilité d’implémenter une logique très complexe pour la journalisation et d’être sûr qu’elle ne sera appelée que lorsque cela est nécessaire.

Function 

L’interface Function vous permet de définir une fonction qui prend un paramètre et produit un résultat. Voici l’interface Function (en omettant certaines méthodes par défaut) :

public interface Function<T, R>{
   R apply(T t);
}

Pour commencer à explorer l’interface Function, examinons une classe responsable du calcul du prix d’un article dans une commande de vente. 

Cette classe prend en entrée des éléments tels que le produit, la quantité et la remise (de 0 à 100) à appliquer :

public class PriceCalculator{
   public BigDecimal calculatePrice(Product product, 
                                    Integer quantity,
                                    BigDecimal discount){
     var grossPrice = product.getUnitPrice()
                             .multiply(BigDecimal.valueOf(quantity));
     var discountAmount = grossPrice.multiply(discount)
                                    .divide(BigDecimal.valueOf(100));
     return grossPrice.minus(discountAmount);
   }
}

// Exemple d'utilisation
var result = priceCalculator(product, 10, BigDecimal.value(10));

Cette classe calcule d’abord le prix brut, applique la remise, puis soustrait celle-ci du prix brut. Maintenant, considérons une nouvelle exigence : effectuer une conversion de devise sur le prix.

Une approche pourrait consister à ajouter directement la logique de conversion de devise à cette classe, introduisant potentiellement des bugs.
Une solution plus robuste consiste à introduire un paramètre Function responsable de la gestion de la conversion de devise.

public class PriceCalculator{
   public BigDecimal calculatePrice(
                        Product product, 
                        Integer quantity, 
                        BigDecimal discount, 
                        Function<BigDecimal,BigDecimal> converterFunction){
     var grossPrice = product.getUnitPrice()
                             .multiply(BigDecimal.valueOf(quantity));
     var discountAmount = grossPrice.multiply(discount)
                                    .divide(BigDecimal.valueOf(100));
     var netPrice = grossPrice.minus(discountAmount);
     return converterFunction.apply(netPrice);
   }
}

// Exemple d'utilisation
var result = priceCalculator(product, 
                             10, 
                             BigDecimal.value(10),
                             netPrice -> netPrice.multiply(CURRENCY_RATE));

L’ajout de cette nouvelle exigence a un impact minimal, et nous avons réussi à inverser la dépendance.
La classe PriceCalculator n’a plus besoin de gérer la conversion de devise ; elle se contente d’appeler la fonction fournie avec le prix net et de renvoyer le résultat.
Cette conception nous permet d’utiliser la même classe PriceCalculator pour convertir n’importe quelle devise sans modifier son code.

Il existe différentes approches pour répondre à cette exigence sans modifier la classe PriceCalculator. Vous pouvez créer une autre classe qui agit comme une façade en appelant la PriceCalculator puis en effectuant la conversion de devise.
En général, c’est une décision de projet de savoir quelle solution suivre.

Consumer 

L’interface Consumer nous permet de définir une fonction qui prend un paramètre, effectue une tâche spécifique et ne renvoie aucune valeur. 

Voici l’interface Consumer (en omettant certaines méthodes par défaut) :

public interface Consumer<T>{
   void accept(T t);
}

Pour voir un exemple de l’interface Consumer en action, examinons cette classe, qui définit certaines informations dans une entité et la sauvegarde dans la base de données :

public class EntitySaver{
   public void create(Entity entity){
      entity.setCreationDate(new Date());
      database.insert(entity);
   }
}

// Exemple d'utilisation
entitySaver.create(entity);

Maintenant, supposons que nous devons notifier d’autres classes chaque fois qu’une entité est créée, mais que nous ne pouvons pas modifier l’interface de la méthode create.
Dans de tels scénarios, nous pouvons mettre en œuvre le modèle de publication-abonnement, en utilisant l’interface Consumer.
Voici comment nous pouvons y parvenir :

public class EntitySaver{
   private List<Consumer<Entity>> consumerList = new ArrayList<>();

   public void register(Consumer<Entity> consumer){
      consumerList.add(consumer);
   }

   public void create(Entity entity){
      entity.setCreationDate(new Date());
      database.insert(entity);
      consumerList.forEach(consumer -> consumer.accept(entity));
   }
}

// Exemple d'utilisation
entitySaver.register(entity -> log.info(entity));
entitySaver.register(entity -> mailerService.notifyUser(entity));
entitySaver.create(entity);

Dans cette mise en œuvre du modèle de publication-abonnement, nous utilisons l’interface Consumer. La classe EntitySaver maintient maintenant une liste de Consumers et comprend une méthode register pour ajouter des consumers à cette liste. 

Alors que l’interface de la méthode create reste inchangée, nous introduisons une seule ligne de code pour “consommer” l’entité créée en invoquant les consumers enregistrés.

Conclusion 

Les interfaces fonctionnelles Java ont été introduites il y a de nombreuses années, et elles ont apporté un grand changement dans la façon dont nous développions en Java. 

Nous pouvons les utiliser comme des fonctions lambda, mais aussi pour inverser les dépendances, et rendre notre code plus propre. 



Aucun commentaire:

Enregistrer un commentaire

to criticize, to improve