Connaissez-vous les revues de code ? Il s’agit d’une relecture du code que vous avez écrit par un autre développeur. Chez CubeCrafters Interactive c’est une obligation qui est mise en place sur chaque projet !
Marc se lance donc dans la revue de code. Quel est son objectif ? Vérifier la qualité du code. Est-ce que le code suit les bonnes pratiques de développement en Java? Une fois sa revue terminée, vous recevez ce message via Slack :
Merci pour ton travail sur le projet Epicrafter’s Journey ! Jusqu’à présent je suis satisfait. J’ai une suggestion, pourrais-tu adapter le code pour utiliser les avantages de la programmation fonctionnelle en Java ? Merci
Il ne vous reste plus qu’à découvrir ce qu’est la programmation fonctionnelle et réussir à mettre cela en œuvre dans le projet.
Découvrez les interfaces fonctionnelles
La programmation fonctionnelle est un paradigme différent de la programmation structurée et de la programmation orientée objet.
Dans certains chapitres précédents, nous avons illustré différents concepts grâce à l’exemple d’une maison. Reprenons cet exemple. Un programme codé avec la programmation fonctionnelle pourrait être comparé à une maison où tous les meubles sont fixés au sol. Ainsi quel que soit l’usage que les habitants font de la maison, cela n’aurait jamais d’impact sur l’agencement de la maison et de ses meubles. Ils sont immuables !
De la même façon, la programmation fonctionnelle repose grandement sur le concept de l’immuabilité :
les fonctions ne changent pas l’état du programme mais retourne le résultat d’un traitement en fonction de données entrantes.
les données sont immuables : étant donné que les fonctions ne changent pas l’état du programme, le paradigme n’autorise pas non plus que l’on change les données entrantes.
Une maison où les meubles sont fixés au sol est la garantie d’une maison fiable en ce sens qu’aucun habitant ne pourra altérer son fonctionnement. On ne retrouvera jamais la table de la salle à manger dans la chambre par exemple ou encore on ne pourra pas débrancher et retirer le lave-linge.
De même, la programmation fonctionnelle a l’avantage d'empêcher la mutabilité de données (autrement dit de changer les valeurs des données après leur initialisation) et ainsi de rendre plus fiable votre programme.
En quoi avoir des données immuables rend fiable un programme ?
Puisque vos meubles sont fixés au sol, il est impossible que deux habitants de la maison veuillent déplacer le même meuble à deux endroits différents, ce qui créerait un conflit. De même, l’immuabilité dans un programme informatique est une protection face aux problèmes d’accès concurrents.
Notez également que la syntaxe d’écriture est plus légère. Et dans le cadre de notre projet Epicrafter’s Journey, c'est surtout ce dernier avantage qui nous intéresse : alléger la syntaxe de notre code !
Je crois que cela mérite maintenant d’être illustré et voir comment utiliser ce paradigme en Java !
En premier lieu, je vous présente les interfaces fonctionnelles !
Mais on connaît déjà les interfaces ?
Oui ! Les interfaces fonctionnelles ont la particularité de définir une seule méthode abstraite.
Faisons un simple exemple :
IDisplay.java
@FunctionnalInterface
public interface IAffichage {
void afficher(String texte);
}
Il n’y a rien de techniquement nouveau pour vous si ce n’est l’annotation @FunctionnalInterface qui d’ailleurs est informative. Cela signifie qu’elle n’a pas d’impact sur le comportement du code exécuté.
Il existe de nombreuses interfaces fonctionnelles dans le langage Java, par exemple :
Predicate dont la méthode prend un paramètre générique de type T et renvoie un booléen.
Function dont la méthode prend un paramètre générique de type T et renvoie une valeur générique de type R.
Consumer dont la méthode prend un paramètre générique de type T et ne renvoie rien.
Runnable dont la méthode ne prend pas de paramètre et ne renvoie rien.
Nous allons généralement nous appuyer sur ces interfaces fonctionnelles existantes plutôt que d'en créer de nouvelles.
D'accord, mais je ne vois pas le lien entre un code allégé et les interfaces fonctionnelles ?
En effet, c’est maintenant le moment de voir comment tirer profit des interfaces fonctionnelles grâce aux expressions lambda pour alléger le code !
Écrivez une expression lambda
Une expression lambda est une fonction anonyme car elle n’a pas besoin d’être définie dans le cadre d’une classe. Grâce à cela, cette fonction est considérée comme une valeur qu’on peut affecter à une variable ou passé en paramètre d’une autre fonction.
Une fonction classique dans une classe pourrait être comparée à un mode de cuisson préprogrammé sur votre four (180°, chaleur tournante, 1h) là où une fonction anonyme serait un choix de cuisson sur le moment, ce choix n’étant pas préprogrammé, on le considère comme anonyme.
Maintenant, rappelons-nous qu'en Java tout est typé. C'est là où nos interfaces fonctionnelles vont entrer en jeu. L’unique méthode déclarée dans l’interface fonctionnelle servira de signature à la fonction anonyme. Ainsi l’interface fonctionnelle est le type associé à une expression lambda.
Même si l’on ne veut pas utiliser de cuisson préprogrammée, notre four nous impose tout de même de choisir la température et la durée de cuisson. De même, l’interface fonctionnelle imposera à la fonction anonyme ses paramètres d’entrées et son type retour.
Si vous êtes décontenancé par de telles définitions, pas de panique ! J’ai essayé de vous faciliter la compréhension avec des illustrations, mais il est reconnu que passer de la programmation orientée objet à la programmation fonctionnelle peut être un vrai challenge.
Laissez-moi vous montrer par la pratique la mise en œuvre de la programmation fonctionnelle dans la démonstration qui suit :
Que retenir de cette démonstration ?
La syntaxe d’une lambda est (paramètres) -> { action }
La méthode forEach utilisée avec les collections prend en paramètre une fonction anonyme qui respecte l’interface fonctionnelle Consumer.
On a passé la lambda suivante en paramètre de forEach : (motCle) -> { System.out.print(motCle);}
Grâce à cette démonstration, nous commençons à y voir plus clair et nous comprenons ce que signifie transmettre une fonction anonyme en paramètre d’une autre fonction.
Je vous ai également mentionné qu’une lambda peut être affectée à une variable. Le code de la méthode afficherKit pourrait alors être le suivant :
public void afficherKit() {
System.out.println("Nombre de bloc dans le kit : " + this.blocs.size());
System.out.print("Liste des mots clés du kit :");
Consumer<String> fonctionAnonyme = (text) -> {System.out.print(text);};
this.motsCles.forEach(fonctionAnonyme);
}
La lambda a été affectée à une variable nommée fonctionAnonyme typée par l’interface fonctionnelle Consumer.
J’ai ensuite passé en paramètre de forEarch la variable fonctionAnonyme. Et le résultat a été le même.
Faisons un autre exemple, toujours dans le cadre du projet Epicrafter’s Journey. Nous voulons ajouter une nouvelle fonctionnalité. Permettre à un utilisateur de forcer une porte en découvrant un code secret ! Pour cela l’utilisateur devra fournir un code qui sera testé par la classe Porte.
Fournir un code qui sera testé, comment faire ?
Via une lambda, une fonction anonyme que l’on transmettra en paramètre d’une autre fonction !
Commençons par ajouter le code suivant dans la classe Porte :
public void forcerSerrure(Predicate<String> fonction) {
String cleSecrete = “#secret123”;
if(this.verrouille) {
if(fonction.test(cleSecrete)) {
this.verrouille = false;
}
}
}
La méthode forcerSerrure possède une variable cleSecrete qui contient “#secret123”. Si l’utilisateur arrive à fournir une fonction anonyme qui renvoie vrai lorsque cette dernière reçoit la clé secrète alors on ouvre la serrure !
Techniquement les deux points clés sont :
La méthode forcerSerrure prend en paramètre une fonction anonyme typée par l’interface fonctionnelle Predicate<String>. La fonction doit donc renvoyer un booléen et a un paramètre qui est une chaîne de caractères.
La méthode test de la fonction anonyme passée en paramètre permettra de vérifier si le code envoyé par l’utilisateur correspond à la clé secrète. Si oui, on déverrouille la porte !
Allons désormais dans une classe Main qui permet d’appeler forcerSerrure. Il nous faudra fournir une fonction anonyme en paramètre de cette méthode. Cette dernière doit bien évidemment respecter l’interface fonctionnelle Predicate.
package ej;
public class Main {
public static void main(String[] args) {
try {
Porte porte = new Porte(1,1,1,true);
porte.forcerSerrure( (cle) -> {
return cle.matches("[A-Za-z]{3}");
});
porte.forcerSerrure( (cle) -> {
return cle.matches("#[A-Za-z]{6}123");
});
} catch((IllegalBlocException e) {
System.out.println(“Impossible de créer le bloc.”);
}
}
}
L’utilisateur essaye ici de deviner la clé secrète grâce à une regex. Il s’agit d’une expression régulière qui définit un modèle.
Si ce modèle est compatible avec la clé secrète alors la méthode matches
renverra vrai et on déverrouille la porte ! Par contre, si l’utilisateur fournit un modèle erroné, alors le forçage de la serrure ne fonctionne pas !
Lors du premier essai, le modèle [A-Za-z]{3} signifie 3 caractères minuscules ou majuscules de l’alphabet. Et bien sûr la clé secrète est plus complexe, ça ne déverrouille pas la porte.
Lors du deuxième essai, le modèle #[A-Za-z]{6}123 signifie :
Le caractère #
Puis, 6 caractères minuscule ou majuscule de l’alphabet minuscule
Puis, les chiffres 123
Et c’est gagné ! La clé secrète commence bien par un # puis il y a 6 caractères et finit par les chiffres 123.
En conclusion de cette section, nous avons vu deux exemples qui permettent de passer des fonctions anonymes à des fonctions classiques et ainsi mettre en œuvre le paradigme de la programmation fonctionnelle !
Définissez des références à vos méthodes
Nous savons que grâce à la programmation fonctionnelle nous pouvons transmettre une fonction anonyme dite lambda en paramètre d’une autre fonction. Pour cela, la lambda doit respecter l’interface fonctionnelle déclarée par la méthode qui reçoit la lambda.
Et dans la section précédente, nous avons créé des lambdas à chaque fois que nous voulions utiliser ces fonctions qui attendent en entrée une fonction anonyme.
Notre objectif est d’alléger notre code, cependant cela peut être lourd de fournir constamment ces fonctions anonymes. Par exemple dans la section précédente nous avons créé la lambda suivante :
Consumer<String> lambda = (text) -> {System.out.print(text);};
Et il s’agit finalement ici d’un simple appel à la méthode print de System.out. Si nous pouvions dire à notre code d’exécuter directement cette méthode, ne serait-ce pas un raccourci intéressant ?
Absolument, et ce l’est possible grâce aux références aux méthodes !
public void afficherKit() {
System.out.println("Nombre de bloc dans le kit : " + this.blocs.size());
System.out.print("Liste des mots clés du kit :");
this.motsCles.forEach(System.out::print);
}
Waouh ! C’est quoi cette notation ultra-réduite ?
Ahah Je suis bien d’accord, c’est très succinct !
Décortiquons ce code :
out
est un attribut static de la classe System, attribut typé par la classe java.io.PrintStream.La classe java.io.PrintStream possède une méthode print qui a la signature suivante
public void print(String s)
Cette signature est conforme à la méthode définie dans l’interface fonctionnelle Consumer, à savoir un paramètre d’entrée et pas de retour.
La méthode print est donc une fonction compatible pour la méthode forEarch qui prend en paramètre une fonction qui respecte l’interface fonctionnelle Consumer.
Très bien, mais comment passer la méthode print en paramètre de forEach ?
Il faut transmettre la référence à cette fonction et c’est possible grâce à la syntaxe [instance]::[nom de la méthode] donc System.out::print
Dans cet exemple, nous faisons référence à la méthode d’un objet instancié. Mais nous pouvons aussi faire référence à une méthode statique, par exemple Integer::valueOf
que nous avions dans le chapitre sur les collections.
Notre objectif d’alléger notre code sera réellement atteint lorsque nous ferons pleinement usage des références à des méthodes existantes plutôt que de créer constamment nos propres lambdas. Évidemment il y a de nombreux cas d’usage et il est donc normal par moment d’écrire des lambdas.
À vous de jouer
Contexte
Le projet Epicrafter’s Journey a bien progressé et il possède une structure maintenable et évolutive.
Marc a réalisé une revue de code et il pense qu’il est possible d’alléger le code existant. Pour cela vous allez tirer profit de la programmation fonctionnelle !
De plus, la nouvelle fonctionnalité pour forcer une serrure doit être intégrée. Cette dernière correspond en l’implémentation d’une méthode dans la classe Porte qui prendra en paramètre une fonction anonyme respectant l’interface fonctionnelle Predicate.
Reprenez le code réalisé jusque-là et suivez les consignes de Marc !
Consignes
Identifier toutes les itérations réalisées sur une collection.
Alléger l’écriture en utilisant des lambdas.
Intégrer la fonction forcerSerrure présentée dans ce chapitre à la classe Porte.
En résumé
La programmation fonctionnelle allège l'écriture du code et empêche l’altération de l’état d’une donnée.
Les interfaces fonctionnelles sont des interfaces avec une seule méthode abstraite.
Une lambda est une fonction anonyme qui peut être affectée à une variable typée par une interface fonctionnelle.
Une lambda est une fonction anonyme qui peut être transmise en paramètre d’une fonction dont le paramètre est typé par une interface fonctionnelle.
Si une méthode respecte une interface fonctionnelle, la référence à cette méthode peut être affectée à une variable ou transmise en paramètre d’une fonction.
C’est l’heure du dernier chapitre de ce cours et pas des moindres, nous allons parler des Threads !