Blog Arolla

Passer à la vitesse supérieure grâce à la programmation réactive

La programmation réactive a beaucoup retenu l'attention au cours ces dernières années. Elle se base sur la programmation asynchrone pour gérer les flux de données et la propagation du changement. En outre, avec l'apparition des librairies comme Rx, Eclipse Vert.x, Reactor, etc. la programmation fonctionnelle connaît également un véritable succès. Si vous souhaitez savoir quel est le lien entre ce paradigme et les autres, devez-vous en adopter un ou une combinaison de ces paradigmes dans votre prochain projet ou refactoring ? Alors cet article est pour vous.

Every 10 years we give new name to what we always do and get excited about it. - Unknown

Avant de passer directement au sujet, commençons par les bases : les paradigmes de programmation sont un moyen de classer les langages de programmation selon leurs fonctionnalités. En général, les paradigmes majeurs sont l’impératif et le déclaratif. À savoir, il existe d'autres paradigmes considérés comme des sous-types de ces deux derniers.

Le paradigme impératif et déclaratif

Le paradigme impératif est la base de tout langage de programmation. Dans ce style, les développeurs sont habitués à décrire dans un programme ce qu’il faut faire et comment le faire. Java, C, C++, C#, etc. sont des langages de ce paradigme. Le paradigme déclaratif est l’inverse de l’impératif. Dans ce style, les développeurs omettent pour l’ordinateur le "comment" en décrivant le"quoi". SQL, XSLT, etc. sont des langages de ce paradigme.

Le paradigme impératif vs le déclaratif

Pour comprendre ces paradigmes, observons la méthode group() dans sa version impérative et déclarative. L’objectif est de regrouper des mots par longueurs et stocker le résultat dans une Map :

// Main
void algorithm() {
   System.out.println(
        group(List.of(“orange”, “apple”, “lemon”, “pineapple”))
   );
}

// Résultat 
{
    5 = [apple, lemon], 6 = [orange], 9 = [pineapple]
}

Dans la version impérative, nous avons géré l'itération et le programme a exécuté tout ce qu’on a demandé. Par exemple, citons la boucle for, le test if, la création et le stockage des données dans les variables.

//La méthode group() en style impératif
Map <Integer, List > group(List words) { 
  Map <Integer, List > lengthMap = new HashMap<>(); 
  for (String word: words) { 
    Integer length = word.length(); 
    List sameLength = lengthMap.get(length);
    if (sameLength == null) { 
      sameLength = new ArrayList <>(); 
      lengthMap.put(length, sameLength);
    }
    sameLength.add(word);
  }
  return lengthMap;
}

Pour appliquer le style déclaratif, il faut pratiquer une conception qui déplace le "comment" en "quoi";

// La méthode group() en style déclaratif
Map<Integer, List> group(List words) { 
     return words
       .stream()
       .collect(Collectors.groupingBy((String s) -> s.length)); 
}

Dans la version déclarative, nous avons demandé "littéralement" de retourner en regroupant le contenu d’une collection de termes par longueur. En plus, la boucle for a été remplacée par une notation "déclarative" via la méthode stream() de la collection elle-même. Ce qui signifie d'exécuter le code pour chaque élément de la liste sans se soucier de décrire l'implémentation. Ainsi, cette méthode contient une partie basée sur style fonctionnel. En effet, à partir de Java 8 et dans d’autres langages, le style fonctionnel avait été introduit. Ainsi ce style a donné plus de pouvoir aux développeurs en manipulant des collections d’où la brièveté du code.

Le paradigme fonctionnel 

La programmation fonctionnelle est basée sur l’évaluation des fonctions au sens mathématique du terme. Ce style consiste à créer des applications en composant des fonctions dites pures. Une fonction pure a toujours le même retour pour les mêmes arguments. Ainsi, ce style permet d'éviter les états partagés, la mutation des données et les effets de bord.  Il est à noter que ce style est supposé déclaratif sauf que l'utilisation d'un style déclaratif n'équivaut pas à un style fonctionnel. Par exemple, forEach est déclarative en la comparant à une boucle for classique. Scala, Kotlin, JavaScript, Haskell, Elm, F# etc. sont des exemples de langages fonctionnels. Regardez la vidéo de Graham Hutton pour en savoir plus sur l’origine de ce style.  Passons au code et regardons la méthode vote(). L’objectif est d’incrémenter le nombre de votes d’un terme et stocker le résultat dans une Map :

//Main
void algorithm() {
 Map<String, Integer> votes = new HashMap<>();

 vote(votes, "orange");
 vote(votes, "orange");
 vote(votes, "lemon");
 
 System.out.println(votes.get("orange"));
}
// Résultat : 
2

Dans la version impérative, le code décrit les variables et les tests à faire pour incrémenter le vote.

// La méthode vote() en style impératif
void vote(Map<String, Integer> votes, String term) {
 if (!votes.containsKey(term)) {
   votes.put(term, 0);
 }
 votes.put(term, votes.get(term) + 1);
} 

Pour traduire le code en style fonctionnel, utilisons la conception de "comment à quoi". La solution est d'initialiser un compteur de termes ou incrémenter sa valeur. Alors, regardons l’utilisation de merge() dans la solution fonctionnelle.

// La méthode vote() en style fonctionnel
void vote(Map<String, Integer> votes, String term) {
 votes.merge(term, 1,(newCount, oldCount) -> newCount + oldCount);
}

merge() accepte 3 paramètres : une clé, la valeur initiale du compteur pour la clé donnée et une fonction d’ordre supérieur. On ne va pas expliquer le fonctionnement de merge(), mais vous pouvez lire l’article de Tomasz Nurkiewicz pour en savoir plus. Avec ces paradigmes déjà présents dans plusieurs langages, nous déléguons davantage de notions de base de programmation à la machine. Le plus grand bénéfice est clairement la brièveté, la lisibilité et l’élimination des instructions intermédiaires du code.  Jusqu’à ces lignes, nous avons vu le paradigme impératif, déclaratif et fonctionnel. Et donc la question qu’on se posait c'est quoi le lien entre ce qu’on a vu et le paradigme réactif ? La réponse est dans la suite !

Le paradigme réactif

D’où vient-il ?

Regardons l’exemple des variables ci-dessous :

// déclaration des variables                                      
int i, j, k;

// Initialisation des variables
i = 1;
j = 2;

// Définir le résultat
k = i + j;

// mettre à jour une valeur dépendante du résultat
j = 4;
k =? // Que devrait être k?

L’état de k peut ne peut pas refléter sa valeur actuelle après une mise à jour d’une autre variable dépendante. Donc, l’enjeux est la propagation du changement. Prenons un autre exemple plus sophistiqué d’un service qui dépend d’autres APIs pour accomplir une tâche comme décrit dans la figure ci-dessous. Théoriquement, il existe une approche synchrone et une autre asynchrone. Le problème de l’approche synchrone est la prise en otage des ressources tout au long d’exécution d’une requête. Donc, le temps peut être réduit en faisant des requêtes indépendantes et en parallèle. Cela est possible grâce à la programmation asynchrone (callbacks, les promises, etc). Cependant, le code peut devenir illisible et difficile à maintenir à cause de ce qu’on appelle le callback hell. L’approche réactive est la solution à ces problèmes. En effet, ensemble, la programmation réactive, déclarative et fonctionnelle en même temps forment une combinaison de techniques élégantes pour s'attaquer aux problèmes cités auparavant.

Le principe de la programmation réactive

Selon Wikipedia, la programmation réactive est un paradigme de programmation asynchrone. Ces concepts de base sont les flux de données et la propagation du changement. De plus, ce style est basé sur le paradigme déclaratif. En d'autres termes, des composants existent qui produisent et/ou consomment des événements issus d’autres composants au cours du temps.

A list expressed over time is a stream. - André Staltz

Dans ce paradigme, les sources d’événements sont majoritairement comparables aux streams quelles qu'elles soient.  En effet, un flux/ stream peut être construit à partir de n’importe quelle donnée dans un système.

Le paradigme réactif vs l’impératif

Observons les suivants codes ayant comme objectif à filtrer et afficher des données d’une source quand son contenu est alimenté au cours du temps. La version réactive utilise la librairie RxJava.

// Manipulation d’une liste en style impératif

List list = new ArrayList<>();
list.add(1);
list.add(2);

list
    .stream()
    .filter(id -> id % 2 == 0)
    .map(id -> id + 100)
    .forEach(result -> System.out.println("Imperative: " + result));

list.add(3);
list.add(4);

// Résultat
Imperative: 102
// Manipulation d’une source en style réactif

Subject<Integer> list = ReplaySubject.create();
list.onNext(1);
list.onNext(2);

list
 .filter(i -> i % 2 == 0)
 .map(i -> i + 100)
 .subscribe(result -> System.out.println("Reactive: " + result));

list.onNext(3);
list.onNext(4);

// Résultat
Reactive: 102
Reactive: 104

Au point de vue de la syntaxe, les opérateurs sont presque similaires même si l’implémentation est différente dans les deux styles. La version impérative implémente une Collection de type Integer pour stocker le contenu. Par contre, la version réactive utilise une source Subject de type Integer de la librairie RxJava. Remarquons que l’ajout d’un élément à la source après l’opération subscribe() permet de continuer d'afficher les résultats à l’inverse du code impératif.

Les avantages du paradigme réactif

L’astuce, c'est que nous ne travaillons pas avec des collections simples mais avec des structures capables de réagir aux changements. Établissons une analogie avec les voitures : une liste est comme un parking. Il est possible d’enregistrer et vérifier la présence d’une voiture en parcourant la liste. Par contre, dans le réactif les flux sont les rues et les voitures sont en mouvement au cours du temps. Il suffit de s’attacher à un flux et observer les événements pour déclencher une action. D'autre part, adopter le réactif signifie mettre en œuvre des patterns pour structurer la façon dont les données sont gérées.  Aussi, adopter ce paradigme aidera à résoudre de nombreux problèmes auxquels les développeurs sont confrontés quotidiennement comme la concurrence, l'obsolescence des données et la gestion des erreurs, ce qui permet d’écrire un code lisible, maintenable et testable.  En outre, les applications d'aujourd'hui sont responsables de gérer d’énormes volumes de données de milliers d’utilisateurs. Ainsi des opérations comme la composition, et la transformation des flux non-bloquants sont inévitables. D'autre part, la nature de l’expérience utilisateur exige d’avoir des applications rapides.

Checklist pour adopter le paradigme réactif

Le choix final pour adopter ou non le paradigme réactif dépend de la nature de l’application et de l’expérience utilisateur. De plus pour faire simple, les composants "réactifs" peuvent être introduits en douceur dans une application : 

  • Commencez toujours petit et simple, privilégiez la programmation impérative pour les requêtes/ réponses synchrones. Exemple : les services web d’accès à des ressources dans une base de données ou un système de fichiers.
  • Lors de la manipulation des données pour faire des transformations, utilisez la programmation fonctionnelle. Exemple : la sélection et la transformation d’une collection d’objets issus d’une base de données.
  • S'il existe un traitement asynchrone ou lorsqu'un ou plusieurs composants doivent être informés des modifications apportées par un autre composant, utilisez la programmation réactive. Exemple : passer les données à travers un broker de message comme RabbitMQ.
  • S'il s'agit de plusieurs traitements asynchrones qui nécessitent plusieurs transformations, utilisez la programmation réactive dans la partie concernée de l’application. Exemple : le service de recommandation de produits dans un site.
  • Si tous les services doivent être réactifs, il est conseillé d’évaluer l’utilisation d’une solution réactive comme Eclipse Vert.x.
  • Si vous n'êtes pas sûr, revenez à la programmation impérative. Ainsi cela aidera à appliquer un refactoring lorsque la vision et les objectifs sont clairs.

Nous finirons probablement par utiliser tout ce qui précède dans un seul projet modulaire dans un environnement distribué.

Les solutions existantes

Les eXtensions Réactives (RxJava, RxJS, Rx.NET, RxScala, etc.) et Reactive streams (Java 9) offrent une solution pour adopter le style réactif dans certaines parties d’une application basée sur Java. En plus, Spring Webflux (Reactor Project en Java) et Eclipse Vert.x (Java, JavaScript, Groovy, Ruby, Kotlin, Scala) intègrent par défaut ces extensions. Et récemment, R2DBC (Java) a été annoncée afin de fournir une API de programmation réactive destinée aux accès à une bases de données SQL.

Conclusion

Il peut être audacieux d’appeler ce paradigme comme une solution miracle, mais c’est certainement un pas progressif vers des applications modernes. Pour créer une application efficace, il est conseillé d'étudier et choisir la solution et les paradigmes de programmation car il n’y a pas de solution magique pour tout besoin. En effet, chaque paradigme est utile selon le problème et le besoin. Le style réactif peut être difficile, car il oblige un développeur à se familiariser avec un nouveau style de programmation. En outre, les discussions sur les limites de ce paradigme ne seront pas finies. Cependant il ne faut pas ignorer que ce paradigme commence à gagner l'intérêt des acteurs majeurs dans l’industrie logicielle et les communautés open-source. Ainsi il est probable que cela aura un impact positif sur votre sources et projets si vous l'essayez.

Références : 

https://en.wikipedia.org/wiki/List_of_programming_languages_by_type https://developer.ibm.com/articles/j-java8idioms1/ https://ajayiyengar.com/2018/05/05/java-8-imperative-vs-declarative-style-of-coding/ https://codepen.io/HunorMarton/post/imperative-vs-reactive https://www.scnsoft.com/blog/java-reactive-programming https://stackoverflow.com/questions/128057/what-are-the-benefits-of-functional-programming https://www.welcometothejungle.co/en/articles/functional-reactive-programming-architecture https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 https://www.nurkiewicz.com/2019/03/mapmerge-one-method-to-rule-them-all.html https://www.youtube.com/watch?v=eis11j_iGMs https://blog.rcode3.com/blog/vertx-vs-webflux/ Functional Reactive Programming, [Stephen Blackheath and Anthony Jones] Functional Programming for Java Developers: Tools for Better Concurrency, Abstraction, and Agility [Dean Wampler] Reactive Java Programming [Andrea Maglie]  

Plus de publications

Comments are closed.