Passer des fonctions en paramètre

Chapitre 2: Passer du comportement en paramètre de fonction

Ce chapitre couvre

  • Faire face à l’évolution des besoins
  • Comportement en paramètre
  • Classes anonymes
  • Aperçu des expressions lambda
  • Exemples réels : Comparator, Runnable et GUI

Un problème bien connu en génie logiciel est que peu importe ce que vous faites, les exigences de l’utilisateur vont changer. Par exemple, imaginez une application pour aider un agriculteur à gérer son inventaire. L’agriculteur pourrait vouloir une fonctionnalité pour trouver toutes les pommes vertes dans son inventaire. Mais le lendemain, il pourrait vous dire : « En fait, je veux aussi trouver toutes les pommes pesant plus de 150 g.» Deux jours plus tard, le fermier revient et ajoute : « Ce serait vraiment bien si je pouvais trouver toutes les pommes qui sont vertes et plus lourdes que 150 g. « Comment pouvez-vous faire face à ces exigences changeantes ? Idéalement, vous voudriez minimiser votre effort d’ingénierie. En outre, de nouvelles fonctionnalités similaires devraient être simples à mettre en œuvre et à maintenir sur le long terme.

Le comportement en paramètre est un pattern de développement logiciel qui vous permet de gérer les changements fréquents du besoin. En un mot, cela signifie prendre un bloc de code et le rendre disponible sans l’exécuter. Ce bloc de code peut être appelé plus tard par d’autres parties de vos programmes, ce qui signifie que vous pouvez différer l’exécution de ce bloc de code. Par exemple, vous pouvez passer le bloc de code en tant qu’argument à une autre méthode qui l’exécutera plus tard. Par conséquent, le comportement de la méthode est paramétré en fonction de ce bloc de code. Par exemple, si vous traitez une collection, vous pouvez écrire une méthode qui

  • Peut faire « quelque chose » pour chaque élément d’une liste
  • Peut faire « autre chose » lorsque vous terminez le traitement de la liste
  • Peut faire « encore autre chose » si vous rencontrez une erreur

C’est ce à quoi se réfère le paramétrage du comportement. Voici une analogie : votre colocataire sait comment vouz conduire au supermarché et rentrer à la maison. Donc, vous pouvez lui dire d’acheter une liste de choses comme le pain, le fromage et le vin. Cela revient à appeler une méthode goAndBuy avec une liste de produits en argument. Mais un jour vous êtes au bureau et vous avez besoin de lui pour faire quelque chose qu’il n’a jamais fait auparavant : ramasser un paquet du bureau de poste. Vous devez maintenant lui transmettre une liste d’instructions : allez au bureau de poste, utilisez ce numéro de référence, parlez au responsable et récupérez le colis. Vous pouvez lui transmettre la liste des instructions par e-mail, et quand il la reçoit, il peut lire et suivre les instructions. Vous avez maintenant fait quelque chose d’un peu plus avancé qui équivaut à une méthode : goAndGetParcel(), qui peut prendre différents nouveaux comportements et les exécuter.

Nous commençons le chapitre en vous présentant un exemple de la façon dont vous pouvez faire évoluer votre code afin d’être plus flexible lors de l’évolution des besoins. En s’appuyant sur ces connaissances, nous montrons comment utiliser le passage de comportement en paramètre pour plusieurs exemples réels. Par exemple, vous avez peut-être déjà utilisé le modèle de paramétrage du comportement en utilisant des classes et des interfaces existantes dans l’API Java pour trier une liste, filtrer des noms de fichiers ou pour dire à un thread d’exécuter un bloc de code ou même de gérer un événement graphique. Vous allez bientôt réaliser que l’utilisation de ce modèle est verbeuse en Java pour le moment. Les expressions lambda dans Java 8 abordent le problème de la verbosité. J’expliquerai au chapitre 3 comment construire des expressions lambda, où les utiliser et comment rendre votre code plus concis en les adoptant.

 

2.1. Faire face à l’évolution des besoins

Il est difficile d’écrire du code qui peut faire face à l’évolution des besoins. Passons à travers un exemple que nous allons progressivement améliorer, en montrant quelques bonnes pratiques pour rendre votre code plus flexible. Dans le contexte d’une application d’inventaire d’une épicerie, vous devez implémenter une fonctionnalité pour filtrer les pommes vertes à partir d’une liste. Facile non ?

2.1.1. Première tentative: filtrer les pommes vertes

Une première solution pourrait être la suivante:

La ligne en surbrillance indique la condition requise pour sélectionner des pommes vertes. Mais maintenant, le fermier change d’avis et veut également filtrer les pommes rouges. Que pouvez-vous faire ? Une solution naïve serait de dupliquer votre méthode, de la renommer en filterRedApples et de changer la condition if pour qu’elle corresponde aux pommes rouges. Néanmoins, cette approche ne s’adapte pas bien aux changements si l’agriculteur veut plusieurs couleurs : vert clair, rouge foncé, jaune, etc. Un bon principe est le suivant : après avoir écrit du code similaire, essayez d’y rajouter de l’abstraction.

2.1.2. Deuxième tentative: paramétrer la couleur

Ce que vous pourriez faire est d’ajouter un paramètre à votre méthode pour paramétrer la couleur et être plus flexible à de telles changements :

Vous avez satisfait le fermier. Il peut utiliser la méthode comme ceci:

Trop facile, non ? Complétez un peu l’exemple. Le fermier vous revient et dit : « Ce serait vraiment cool de faire la différence entre les pommes légères et les pommes lourdes. Les pommes lourdes ont généralement un poids supérieur à 150 g. » En portant votre chapeau de génie logiciel, vous vous rendez compte à l’avance que l’agriculteur peut vouloir filtrer à partir du poids. Du coup vous créez la méthode suivante pour pouvoir passer en paramètre le poids des fruits :

C’est une bonne solution, mais voyez comment vous devez dupliquer la plupart des implémentations pour itérer sur l’inventaire et appliquer les critères de filtrage sur chaque pomme. C’est un peu décevant car cela brise le principe du « DRY » (don’t repeat yourself) de l’ingénierie logicielle. Que se passe-t-il si vous souhaitez modifier le filtre pour améliorer les performances ? Vous devez maintenant modifier l’implémentation de toutes vos méthodes au lieu d’une seule. Cela coûte cher du point de vue de l’effort d’ingénierie. Vous pouvez combiner la couleur et le poids en une seule méthode appelée filtre. Mais alors vous auriez toujours besoin d’un moyen de différencier l’attribut sur lequel vous voulez filtrer. Vous pouvez ajouter un flag pour différencier les requêtes de couleur et de poids. (Mais ne faites jamais ça. Je vous expliquerai pourquoi bientôt.)

2.1.3. Troisième tentative: filtrer avec chaque attribut qui vous vient à l’esprit

Notre horrible tentative de fusionner tous les attributs apparaît comme ceci:

On obtient:

Cette solution est extrêmement mauvaise. Tout d’abord, le code client semble terrible. Que veulent dire le vrai et le faux ? De plus, cette solution ne s’adapte pas à l’évolution des besoins. Que faire si le fermier vous demande de filtrer avec différents attributs d’une pomme, par exemple sa taille, sa forme, son origine, etc. ? De plus, que se passe-t-il si le fermier vous demande des requêtes plus compliquées qui combinent des attributs, comme des pommes vertes qui sont aussi lourdes ? Vous disposez de plusieurs méthodes de filtre dupliquées ou d’une méthode géante et très complexe. Jusqu’à présent, vous avez paramétré la méthode filterApples avec des valeurs telles qu’une chaîne, un entier ou un booléen. Cela peut être bon pour certains problèmes bien définis. Mais dans ce cas, vous avez besoin d’une meilleure façon de dire à votre méthode filterApples les critères de sélection des pommes. Dans la section suivante, nous décrivons comment utiliser le paramétrage du comportement pour atteindre cette flexibilité.

2.2. Paramétrage du comportement

Vous avez vu dans la section précédente que vous avez besoin d’un meilleur moyen que d’ajouter juste des paramètres pour faire face à l’évolution des besoins. Revenons en arrière et trouvons un meilleur niveau d’abstraction. Une solution possible est de modéliser vos critères de sélection : vous travaillez avec des pommes et retournez un booléen basé sur certains attributs d’Apple (par exemple, est-ce que le vert est plus lourd que 150 g ?). Nous appelons cela un prédicat (c’est-à-dire une fonction qui renvoie un booléen). Définissons donc une interface pour modéliser ce critère de sélection :

Vous pouvez maintenant déclarer plusieurs implémentations d’ApplePredicate pour représenter différents critères de sélection, par exemple (et illustré à la figure 2.1) :

Vous pouvez voir ces critères comme des comportements différents pour la méthode de filtre. Ce que vous venez de faire est semblable au design pattern stratégie, qui vous permet de définir une famille d’algorithmes, d’encapsuler chaque algorithme (appelé stratégie) et de sélectionner un algorithme au moment de l’exécution. Dans ce cas, la famille d’algorithmes est ApplePredicate et les différentes stratégies sont AppleHeavyWeightPredicate et AppleGreenColorPredicate.

Mais comment pouvez-vous utiliser les différentes implémentations d’ApplePredicate ? Vous avez besoin de votre méthode filterApples pour accepter les objets ApplePredicate afin de tester une condition sur une Apple. C’est ce que signifie la paramétrisation du comportement : la capacité de dire à une méthode de prendre en compte plusieurs comportements (ou stratégies) et de les utiliser en interne pour accomplir différents comportements.

Pour cela, dans l’exemple en cours d’exécution, vous ajoutez un paramètre à la méthode filterApples pour prendre un objet ApplePredicate. Cela présente un grand avantage en matière d’ingénierie logicielle : vous pouvez maintenant séparer la logique d’itération de la collection dans la méthode filterApples avec le comportement que vous souhaitez appliquer à chaque élément de la collection (dans ce cas un prédicat).

2.2.1. Quatrième tentative: filtre par critères abstraits

Notre méthode de filtre modifiée, qui utilise un ApplePredicate, ressemble à ceci:

Vous avez réalisé quelque chose de vraiment cool : le comportement de la méthode filterApples dépend du code que vous lui transmettez via l’objet ApplePredicate. En d’autres termes, vous avez paramétré le comportement de la méthode filterApples ! Notez que dans l’exemple précédent, le seul code qui compte vraiment est la mise en œuvre de la méthode de test, comme illustré dans la figure 2.2; c’est ce qui définit les nouveaux comportements de la méthode filterApples. Malheureusement, parce que la méthode filterApples ne peut prendre que des objets, vous devez envelopper ce code dans un objet ApplePredicate. Ce que vous faites est similaire au « passage de code » inline, parce que vous passez une expression booléenne à travers un objet qui implémente la méthode de test. Vous verrez dans la section 2.3 (et plus en détail au chapitre 3) qu’en utilisant les lambdas, vous pourrez passer directement l’expression « «red ».equals (apple.getColor ()) && apple.getWeight ()> 150 à la méthode filterApples sans avoir à définir plusieurs implémentations de l’interface ApplePredicate et ainsi éviter toute cette verbosité.

Comme je l’ai expliqué précédemment, le paramétrage du comportement est idéal car il permet de séparer la logique d’itération de la collection pour filtrer et le comportement à appliquer sur chaque élément de cette collection. En conséquence, vous pouvez réutiliser la même méthode et lui donner des comportements différents pour réaliser différentes choses, comme illustré à la figure 2.3. C’est pourquoi le paramétrage du comportement est un concept utile que vous devriez avoir dans votre boîte à outils pour créer des API flexibles.

Pour être sûr que vous avez compris le paramétrage du comportement de java 8, je vous ai préparé un petit Quiz 2.1



Questionnaire 2.1: Écrire une méthode prettyPrintApple qui prend une liste de pommes et qui peut être paramétrée de plusieurs façons pour générer une chaîne de caractères en sortie à partir d’une pomme (un peu comme plusieurs méthodes toString personnalisées). Par exemple, vous pouvez dire à votre jolie méthode PrintApple d’imprimer uniquement le poids de chaque pomme. Ou alors, vous pouvez indiquer à votre méthode prettyPrintApple d’imprimer chaque pomme individuellement et de mentionner si elle est lourde ou légère. La solution est similaire aux exemples de filtrage que nous avons explorés jusqu’à présent. Pour vous aider à démarrer, je vous ai fournis un squelette approximatif de la méthode :

Réponse :

Tout d’abord, vous avez besoin d’un moyen de représenter un comportement qui prend un Apple et retourne un résultat String formaté. Vous avez fait quelque chose de similaire lorsque vous avez créé l’nterface ApplePredicate :

Vous pouvez maintenant représenter plusieurs comportements de mise en forme en implémentant l’interface AppleFormatter :

Enfin, vous devez indiquer à votre méthode prettyPrintApple de prendre en paramètre les objets AppleFormatter et les utiliser en interne :

Vous pouvez maintenant transmettre plusieurs comportements à votre méthode prettyPrintApple. Pour ce faire, instanciez les implémentations d’AppleFormatter et donnez-les comme arguments à la méthode prettyPrintApple :

On obtiendra le résultat suivant:

Ou alors essayez ceci:

Et on obtiendra:



Vous avez vu que vous pouvez rajouter une couche d’abstraction au dessus du comportement et adapter votre code aux modifications des exigences, mais le processus est verbeux car vous devez déclarer plusieurs classes que vous instanciez une seule fois. Voyons comment l’améliorer.

2.3 S’attaquer à la verbosité

Nous savons tous qu’une fonctionnalité ou un concept qui est lourd à utiliser sera laisser à l’abandon par les développeurs. À l’heure actuelle, lorsque vous souhaitez transmettre un nouveau comportement à votre méthode filterApples, vous devez déclarer plusieurs classes qui implémentent l’interface ApplePredicate et instancier plusieurs objets ApplePredicate, que vous allez utiliser une seule fois, comme indiqué dans la liste suivante qui résume ce que vous avez vu jusqu’ici. On remarque qu’il y a effectivement beaucoup de verbosité.

C’est juste horrible. Pouvez-vous faire mieux ? Java a un mécanisme appelé classes anonymes, qui vous permettent de déclarer et d’instancier une classe en même temps. Ils vous permettent d’améliorer votre code un peu plus en le rendant un peu plus concis. Mais ils ne sont pas entièrement satisfaisants. La section 2.3.3 montre un court aperçu de la façon dont les expressions lambda peuvent rendre votre code plus lisible avant d’en discuter plus en détail dans le chapitre suivant.

2.3.1. Classes anonymes

Les classes anonymes sont comme les classes locales (une classe définie dans un bloc) que vous connaissez déjà en Java. Mais les classes anonymes n’ont pas de nom. Ils vous permettent de déclarer et d’instancier une classe en même temps. En d’autres termes, ils vous permettent de créer des implémentations jetables en quelque sorte.

2.3.2. Cinquième tentative: utiliser une classe anonyme

Le code suivant montre comment réécrire l’exemple de filtrage en créant un objet qui implémente ApplePredicate à l’aide d’une classe anonyme :

Les classes anonymes sont souvent utilisées dans le contexte des applications GUI pour créer des objets de gestion d’événements (ici en utilisant l’API JavaFX, une plate-forme d’interface utilisateur moderne pour Java) :

Deuxièmement, de nombreux programmeurs les trouvent difficile à utiliser. Par exemple, le Quiz 2.2 montre un casse-tête Java classique qui surprend la plupart des programmeurs ! Essayez vous. Je suis moi même tombé sur cette problématique lors de ma première mission chez BforBank.



Quiz 2.2: Puzzle de classe anonyme

Quelle sera la sortie quand ce code sera exécuté: 4, 5, 6 ou 42?

Répondre : La réponse est 5, parce que cela fait référence à Runnable englobant, pas à la classe englobante MeaningOfThis.



La verbosité en général est mauvaise ; cela décourage l’utilisation d’une fonctionnalité du langage car il faut beaucoup de temps pour écrire et maintenir un code précis et ce n’est pas agréable à lire! Un bon code devrait être facile à comprendre en un coup d’œil. Même si les classes anonymes abordent un peu la verbosité associée à la déclaration de plusieurs classes concrètes pour une interface, elles ne sont toujours pas satisfaisantes. Dans le contexte du passage d’une simple partie de code (par exemple une expression booléenne représentant un critère de sélection), vous devez toujours créer un objet et implémenter explicitement une méthode pour définir un nouveau comportement (par exemple, le test de méthode pour Predicate ou la méthode handle pour EventHandler). Idéalement, nous aimerions encourager les programmeurs à utiliser le modèle de paramétrage du comportement car, comme vous venez de le voir, cela rend votre code plus adaptable aux changements d’exigences. Dans le chapitre 3, vous verrez que les concepteurs de langage Java 8 ont résolu ce problème en introduisant des expressions lambda, un moyen plus concis de passer du code. Assez de suspense ; voici un bref aperçu de la façon dont les expressions lambda peuvent vous aider dans votre quête d’un code propre.

2.3.3. Sixième tentative: utiliser une expression lambda

Le code précédent peut être réécrit comme ceci dans Java 8 en utilisant une expression lambda :

Vous devez admettre que ce code semble beaucoup plus propre que nos tentatives précédentes. C’est génial parce que ça commence à être beaucoup plus proche de l’énoncé du problème. Nous avons maintenant résolue la question de la verbosité. La figure 2.4 résume notre voyage jusqu’à présent.

2.3.4. Septième tentative: faire une abstraction sur le type de liste

Il y a un pas de plus que vous pouvez faire dans votre voyage vers l’abstraction. Pour le moment, la méthode filterApples ne fonctionne que pour Apple. Mais vous pouvez également faire une abstraction sur le type de la liste pour pouvez effectuer le même filtre peut importe le type de la liste que vous avez :

Vous pouvez maintenant utiliser le filtre de méthode avec une liste de bananes, d’oranges, d’entiers ou de Strings ! Voici un exemple, en utilisant des expressions lambda :

Vous avez réussi à trouver le bon endroit entre flexibilité et concision, ce qui n’était pas possible avant Java 8 !

2.4. Exemples concrets

Vous avez maintenant vu que le paramétrage du comportement est un pattern utile pour s’adapter facilement à l’évolution des besoins. Ce pattern vous permet d’encapsuler un comportement (un morceau de code) et de paramétrer le comportement des méthodes en passant et en utilisant ces comportements que vous avez crés (par exemple, des prédicats différents pour une pomme). Nous avons mentionné plus tôt que cette approche est similaire au design pattern stratégie. Vous avez peut-être déjà utilisé ce pattern dans la pratique. De nombreuses méthodes de l’API Java peuvent être paramétrées avec différents comportements. Ces méthodes sont souvent utilisées avec des classes anonymes. Je montre trois exemples, qui devraient renforcer l’idée de passer du code en paramètre : le tri avec un comparateur, l’exécution d’un bloc de code avec Runnable, et la gestion des événements GUI.

2.4.1. Tri avec un comparator interface

Le tri d’une collection est une tâche de programmation récurrente. Par exemple, supposons que votre agriculteur veuille que vous triiez l’inventaire des pommes en fonction de leur poids. Ou peut-être qu’il change d’avis et veut que vous triiez les pommes par couleur.

Ça vous semble familier ? Oui, vous avez besoin d’un moyen de représenter et d’utiliser différents comportements de tri pour vous adapter facilement à l’évolution des besoins.

Dans Java 8, une liste est fournie avec une méthode de tri (vous pouvez également utiliser Collections.sort). Le comportement de sort peut être paramétré en utilisant un objet java.util.Comparator, qui a l’interface suivante :

Vous pouvez donc créer différents comportements pour la méthode de tri en créant une implémentation ad hoc de Comparator. Par exemple, vous pouvez l’utiliser pour trier l’inventaire en augmentant le poids en utilisant une classe anonyme :

Si le fermier change d’avis sur le tri des pommes, vous pouvez créer un comparateur ad hoc pour qu’il corresponde à la nouvelle exigence et le transmettre à la méthode de tri. Les détails internes sur la façon de trier sont couverts par une couche d’abstraction, ici l’interface Comparator. Avec une expression lambda cela ressemblerait à ceci :

Encore une fois, ne vous inquiétez pas pour cette nouvelle syntaxe ; le chapitre suivant décrit en détail comment écrire et utiliser des expressions lambda.

2.4.2. Exécuter un bloc de code avec Runnable

Les threads sont comme un processus léger : ils exécutent eux-mêmes un bloc de code. Mais comment pouvez-vous dire à un thread quel bloc de code exécuter ? Plusieurs threads peuvent exécuter un code différent. Ce dont vous avez besoin est une façon de représenter un morceau de code à exécuter plus tard. En Java, vous pouvez utiliser l’interface Runnable pour représenter un bloc de code à exécuter ; notez que le code ne renverra aucun résultat (void) :

Vous pouvez utiliser cette interface pour créer différents threads avec des comportements différents comme ceci :

Avec une Lambda expression on aurait eu ceci:

2.4.3. Gestion des événements GUI

Un modèle typique de programmation GUI consiste à effectuer une action en réponse à un certain événement, tel qu’un clic ou un survol du texte. Par exemple, si l’utilisateur clique sur le bouton « Envoyer », vous pouvez afficher une fenêtre contextuelle ou enregistrer l’action dans un fichier. Encore une fois, vous avez besoin d’un moyen de faire face aux changements ; Dans JavaFX, vous pouvez utiliser un EventHandler pour associer une réponse à un événement en le passant à setOnAction :

Ici, le comportement de la méthode setOnAction est paramétré avec des objets EventHandler. Avec une expression lambda cela ressemblerait à ceci :

2.5. Résumé

Voici les principaux concepts que vous devriez retirer de ce chapitre :

  • Le paramétrage du comportement est la capacité d’une méthode à prendre plusieurs comportements différents en paramètre et à les utiliser en interne pour accomplir différentes fonctions.
  • Le paramétrage du comportement vous permet de rendre votre code plus adaptable à l’évolution des besoins et réduit les efforts d’ingénierie à venir.
  • Le paramétrage du comportement est un moyen de donner de nouveaux comportements en tant qu’arguments à une méthode. Mais c’était verbeux avant Java 8. Les classes anonymes ont aidé un peu avant Java 8 à se débarrasser de la verbosité associée à la déclaration de plusieurs classes concrètes pour une interface et qui n’étaient utilisées qu’une seule fois.
  • L’API Java contient de nombreuses méthodes pouvant être paramétrées avec différents comportements, notamment le tri, les threads et la gestion de l’interface graphique.

 

             Précédent                                                                                    Suivant