Refactoring, test et débogage

Chapitre 8: Refactoring, test et débogage

Ce chapitre couvre

  • Comment refactorer le code pour utiliser les expressions lambda
  • L’impact des expressions lambda sur les modèles de conception orientés objet
  • Test d’expressions lambda
  • Débogage du code utilisant les expressions lambda et l’API Streams

Dans les sept premiers chapitres de ce livre, vous avez vu le pouvoir expressif des lambdas et de l’API Streams. Vous étiez principalement en train de créer un nouveau code qui utilisait ces fonctionnalités. Si vous devez démarrer un nouveau projet Java, vous pouvez utiliser lambdas et les flux immédiatement.

Malheureusement, vous ne pouvez pas toujours commencer un nouveau projet à partir de zéro. La plupart du temps, vous devrez faire face à un code existant et écrit dans une ancienne version de Java.

C’est le but de ce chapitre. Il présente plusieurs recettes montrant comment vous pouvez refactorer le code existant pour utiliser les expressions lambda et gagner en lisibilité et en flexibilité. En outre, nous discuterons de comment plusieurs design pattern orientés objet, y compris la stratégie, la méthode template, l’observeur, la chaîne de responsabilité, et la factory peuvent être rendus plus concis grâce aux expressions lambda. Enfin, nous explorerons comment vous pourez tester et déboguer du code qui utilise des expressions lambda et l’API Streams.

8.1. Refactoring pour une meilleure lisibilité et flexibilité

Dès le début de ce tutoriel, nous avons soutenu que les expressions lambda peuvent vous permettre d’écrire un code plus concis et plus flexible, car les expressions lambda permettent de représenter un comportement sous une forme plus compacte par rapport à l’utilisation de classes anonymes. Nous avons également montré au chapitre 3 que les références de méthodes vous permettent d’écrire un code encore plus concis si tout ce que vous voulez faire c’est passer une méthode existante en argument à une autre méthode.

Votre code est plus flexible car les expressions lambda encouragent le paramétrage de comportement que nous avons introduit au chapitre 2. Votre code peut utiliser et exécuter plusieurs comportements passés en tant qu’arguments pour faire face aux changements d’exigences.

Dans cette section, nous rassemblons tout et vous montrons les étapes simples que vous pouvez suivre pour refactoriser le code afin de gagner en lisibilité et flexibilité, ceci en utilisant les fonctionnalités que vous avez apprises dans les chapitres précédents : lambdas, références de méthodes et streams.

8.1.1. Améliorer la lisibilité du code

Qu’est-ce que cela signifie d’améliorer la lisibilité du code ? Il est difficile de définir ce que signifie une bonne lisibilité, car cela peut être très subjectif. L’opinion générale est que cela traduit «la facilité avec laquelle ce code peut être compris par une autre personne ». Améliorer la lisibilité du code signifie s’assurer que votre code est compréhensible et maintenable par des personnes autres que vous. Vous pouvez prendre quelques mesures pour vous assurer que votre code est compréhensible par d’autres personnes, par exemple en vous assurant que votre code est bien documenté et respecte les normes de codage.

Les fonctionnalités de Java 8 peuvent également aider à améliorer la lisibilité du code par rapport aux versions précédentes :

  • Vous pouvez réduire la verbosité de votre code, ce qui le rend plus facile à comprendre.
  • Vous pouvez améliorer l’identification de la fonction de votre code en utilisant des références de méthode et l’API Streams.

Nous décrirons trois refactorings simples qui utilisent les expressions lambdas, les références de méthodes et les flux, que vous pouvez appliquer à votre code pour améliorer sa lisibilité :

  • Refactorisation de classes anonymes en expressions lambda
  • Refactorisation des expressions lambda aux références de méthode
  • Refactorisation du traitement des données d’un style impératif vers l’utilisation de l’API Stream

8.1.2. Des classes anonymes aux expressions lambda

Le premier refactoring simple que vous devriez considérer est la conversion des utilisations de classes anonymes implémentant une seule méthode abstraite en expressions lambda. Pourquoi ? Nous espérons que nous vous avons convaincus dans les chapitres précédents que les classes anonymes sont extrêmement verbeuses et sujets aux erreurs. En adoptant des expressions lambda, vous produisez du code plus succinct et plus lisible. Par exemple, comme indiqué au chapitre 3, voici une classe anonyme pour créer un objet Runnable et son équivalent lambda:

Mais convertir des classes anonymes en expressions lambda peut être un processus difficile dans certaines situations. Premièrement, les significations de this et de super sont différentes pour les classes anonymes et les expressions lambda. Dans une classe anonyme, this fait référence à la classe anonyme elle-même, mais à l’intérieur d’une expression lambda, elle fait référence à la classe qui l’entoure. Deuxièmement, les classes anonymes sont autorisées au shadowing les variables de la classe englobante. Les expressions lambda ne peuvent pas (elles provoquent une erreur de compilation), comme indiqué dans le code suivant :

Enfin, la conversion d’une classe anonyme en une expression lambda peut rendre le code résultant ambigu dans le contexte d’une surchage surcharge. En effet, le type de classe anonyme est explicite à l’instanciation, mais le type de lambda dépend de son contexte. Voici un exemple de comment cela peut être problématique. Disons que vous avez déclaré une interface fonctionnelle avec la même signature que Runnable, appelée ici Task (cela peut se produire lorsque vous avez besoin de noms d’interface plus significatifs dans votre modèle de domaine) :

Vous pouvez maintenant passer une tâche d’implémentation de classe anonyme sans problème :

Mais la conversion de cette classe anonyme en une expression lambda entraîne un appel de méthode ambigu, car Runnable et Task sont des types de cibles valides :

Vous pouvez résoudre l’ambiguïté en fournissant un type explicite (Task) :

Ne vous laisser pas abattre par ces problèmes cependant ; il y a de bonnes nouvelles! La plupart des environnements de développement intégrés (IDE) tels que NetBeans et IntelliJ prennent en charge ce refactoring et garantissent automatiquement que ces pièges ne se produisent pas.

8.1.3. Des expressions lambda aux références de méthode

Les expressions lambda sont parfaites pour un code court qui doit être transmis. Mais pensez à utiliser des références de méthode lorsque cela est possible pour améliorer la lisibilité du code. Un nom de méthode indique plus clairement l’intention de votre code. Par exemple, au chapitre 6, nous vous avons montré le code suivant pour regrouper les plats par niveau calorique :

 

Vous pouvez extraire l’expression lambda dans une méthode séparée et la passer en argument à groupingBy. Le code devient plus concis et son intention est maintenant plus explicite :

Vous devez ajouter la méthode getCaloricLevel dans la classe Dish pour que cela fonctionne:

En outre, pensez à utiliser des méthodes statiques auxiliaires telles que comparing et maxBy lorsque cela est possible. Ces méthodes ont été conçues pour être utilisées avec des références de méthode. En effet, ce code énonce beaucoup plus clairement son intention que son homologue en utilisant une expression lambda, comme nous l’avons montré au chapitre 3 :

De plus, pour de nombreuses opérations de réduction courantes telles que sum, maximum, il existe des méthodes d’aide intégrées qui peuvent être combinées avec des références de méthode. Par exemple, nous avons montré que l’utilisation de l’API Collectors permet de trouver le maximum ou la somme de manière plus claire que d’utiliser une combinaison d’une expression lambda et d’une opération de réduction de niveau inférieur. Au lieu d’écrire

Essayez d’utiliser d’autres collecteurs intégrés, qui énoncent plus clairement l’énoncé du problème. Ici, nous utilisons le collecteur SummingInt (les noms jouent un rôle essentiel dans la documentation de votre code) :

8.1.4. Du traitement de données impératif aux Streams

Idéalement, vous devriez essayer de remplacer tout le code qui traite une collection avec un itérateur pour utiliser par l’API Streams à la place. Pourquoi ? L’API Streams exprime plus clairement l’intention d’un pipeline de traitement de données. En outre, les flux peuvent être optimisés en coulisses en utilisant des short-circuiting tout en tirant parti de votre architecture multicœur, comme nous l’avons expliqué au chapitre 7.

Par exemple, le code impératif suivant exprime deux modèles (filtrage et extraction) qui sont utilisés ensemble, ce qui oblige le programmeur à bien comprendre l’ensemble de l’implémentation avant de déterminer ce que fait le code. De plus, une implémentation qui s’exécute en parallèle serait beaucoup plus difficile à écrire (voir la section 7.2 du chapitre précédent sur le framework fork/join pour avoir une idée) :

L’alternative utilisant l’API Streams ressemble plus à l’énoncé du problème, et elle peut être facilement parallélisée:

Malheureusement, la conversion du code impératif en Streams API peut être une tâche difficile, car vous devez réfléchir aux instructions de flux de contrôle, telles que break, continue et return, et déduire les opérations de flux correctes à utiliser. La bonne nouvelle est que certains outils peuvent vous aider dans cette tâche.

8.1.5. Améliorer la flexibilité du code

Nous avons soutenu dans les chapitres 2 et 3 que les expressions lambda encouragent le style de paramétrage du comportement. Vous pouvez représenter plusieurs comportements différents avec des lambdas différentes que vous pouvez ensuite passer en paramètre. Ce style vous permet de gérer les changements d’exigences (par exemple, en créant plusieurs manières différentes de filtrer avec un prédicat ou en utilisant l’interface Comparator pour les comparaisons). Nous examinons maintenant quelques modèles que vous pouvez appliquer à votre code pour bénéficier immédiatement des expressions lambda.

Exécution différée conditionnelle

Il est courant de voir des instructions de flux de contrôle altérées dans le code de la logique métier. Les scénarios typiques incluent les contrôles de sécurité et les logs. Par exemple, considérez le code suivant qui utilise la classe Java Logger intégrée :

Qu’est ce qui ne va pas avec ça ? Un certain nombre de choses :

  • L’état du logger (quel niveau il prend en charge) est exposé dans le code client via la méthode isLoggable.
  • Pourquoi devriez-vous interroger l’état de l’objet logger à chaque fois avant de pouvoir logger un message ? Cela encombre votre code.

Une meilleure alternative consiste à utiliser la méthode log, qui vérifie en interne si l’objet est défini au bon niveau de log avant de logger le message :

C’est une meilleure approche car votre code n’est pas encombré avec la vérification du if, et l’état du logger n’est plus exposé. Malheureusement, il y a toujours un problème avec ce code. Le message de log est toujours évalué, même si l’enregistreur n’est pas activé pour le niveau de message passé en argument.

C’est là que les expressions lambda peuvent aider. Ce dont vous avez besoin est un moyen de différer la construction du message afin qu’il puisse être généré uniquement dans une condition donnée (ici, lorsque le niveau de l’enregistreur est réglé sur FINER). Il s’est avéré que les concepteurs de l’API Java 8 connaissaient ce problème et ont introduit une alternative surchargée à la journalisation qui prend un Supplier comme argument. Cette méthode de log  alternative a la signature suivante :

Vous pouvez maintenant l’appeler comme ceci:

La méthode log exécutera en interne l’expression lambda passée en argument seulement si le logger est du bon niveau. L’implémentation interne de la méthode log est la suivante :

Si vous vous voyez interroger plusieurs fois sur l’état d’un objet dans le code client (par exemple, l’état de l’enregistreur), uniquement pour appeler une méthode sur cet objet avec des arguments (par exemple, logger un message), alors envisagez d’introduire une nouvelle méthode qui appelle cette méthode (passée en tant que référence lambda ou méthode) uniquement après vérification interne de l’état de l’objet. Votre code sera plus lisible (moins encombré) et mieux encapsulé (l’état de l’objet n’est pas exposé dans le code client).

Exécution around

Dans le chapitre 3, nous avons discuté d’un autre modèle de conception que vous pouvez adopter : le pattern exécution around. Si vous vous retrouvez entrain d’entourer un code différent avec les mêmes phases de préparation et de nettoyage, vous pouvez souvent encapsuler ce code dans une expression lambda. L’avantage est que vous réutilisez la logique traitant des phases de préparation et de nettoyage, réduisant ainsi la duplication du code.

Pour vous rafraîchir la mémoire, voici le code que vous avez vu au chapitre 3. Il réutilise la même logique pour ouvrir et fermer un fichier mais peut être paramétré avec différents lambdas pour traiter le fichier :

Cela a été rendu possible en introduisant l’interface fonctionnelle BufferedReader-Processor, qui vous permet de passer différentes lambdas expressions pour travailler avec un objet BufferedReader.

Dans cette section, vous avez vu comment appliquer différentes recettes pour améliorer la lisibilité et la flexibilité de votre code. Vous allez maintenant voir comment les expressions lambdas peuvent souvent réduire le code standard associé aux design pattern orientés objet courants, et ceci de façon considérable.

8.2. Refactoriser des motifs de design orientés objet avec des lambdas

Les nouvelles fonctionnalités du langage rendent souvent les patterns de code ou les idiomes existants moins populaires. Par exemple, l’introduction de la boucle for-each dans Java 5 a remplacé de nombreuses utilisations d’itérateurs explicites car elle est moins sujette aux erreurs et plus concise. L’introduction de l’opérateur de diamant <> dans Java 7 a réduit l’utilisation de génériques explicites lors de la création d’instance (et a lentement poussé les programmeurs Java à adopter l’inférence de type).

Une classe spécifique de pattern est appelée design pattern. Ils sont un modèle réutilisable, si vous voulez, pour un problème commun lors de la conception de logiciels. C’est un peu comme la façon dont les ingénieurs de construction ont un ensemble de solutions réutilisables pour construire des ponts pour des scénarios spécifiques (tels que le pont suspendu, le pont en arc, etc.). Par exemple, le modèle de conception du pattern visiteur est une solution courante pour séparer un algorithme de la structure sur laquelle il doit fonctionner. Le pattern singleton est une solution courante pour limiter l’instanciation d’une classe à un seul objet.

Les expressions lambda fournissent encore un nouvel outil dans la boîte à outils du programmeur. Ils peuvent fournir des solutions alternatives aux problèmes que les schémas de conception abordent mais souvent avec moins de travail et de manière plus simple. De nombreux design pattern orientés objets existants peuvent être redondants ou écrits de manière plus concise à l’aide d’expressions lambda. Dans cette section, nous en explorons cinq :

  • Stratégie
  • Méthode de template
  • Observateur
  • Chaîne de responsabilité
  • Factory

Nous montrerons comment les expressions lambda peuvent fournir un moyen alternatif de résoudre le même problème auquel chaque modèle de conception est destiné.

8.2.1. Stratégie

Le pattern stratégie est une solution courante pour représenter une famille d’algorithmes et vous permettre de choisir parmi eux au moment de l’exécution. Vous avez brièvement vu ce modèle dans le chapitre 2, lorsque nous vous avons montré comment filtrer un inventaire avec différents prédicats (par exemple, des pommes lourdes ou des pommes vertes). Vous pouvez appliquer ce modèle à une multitude de scénarios, tels que la validation d’une entrée avec différents critères, l’utilisation de différentes méthodes de parsing ou le formattage d’une entrée.

Le schéma de stratégie se compose de trois parties, comme illustré à la figure 8.1:

  • Une interface pour représenter un algorithme (l’interface Strategy)
  • Une ou plusieurs implémentations concrètes de cette interface pour représenter plusieurs algorithmes (les classes concrètes ConcreteStrategyA, ConcreteStrategyB)
  • Un ou plusieurs clients utilisant les objets stratégie

Supposons que vous souhaitiez valider si une entrée de texte est correctement formatée pour différents critères (par exemple, elle ne comprend que des lettres minuscules ou est numérique). Vous commencez par définir une interface pour valider le texte (représenté par une String) :

Deuxièmement, vous définissez une ou plusieurs implémentations de cette interface:

Vous pouvez ensuite utiliser ces différentes stratégies de validation dans votre programme:

Utiliser des expressions lambda

Vous devriez maintenant reconnaître que ValidationStrategy est une interface fonctionnelle (en plus, elle a le même descripteur de fonction que Predicate <String>). Cela signifie qu’au lieu de déclarer de nouvelles classes pour implémenter des stratégies différentes, vous pouvez passer directement des expressions lambda, qui sont plus concises :

Comme vous pouvez le voir, les expressions lambda suppriment le code standard inhérent au design pattern stratégie. Nous vous recommandons donc d’utiliser des expressions lambda à la place pour des problèmes similaires.

8.2.2. Méthode de template

Le design pattern de méthode de template est une solution courante lorsque vous devez représenter le contour d’un algorithme et avoir la flexibilité supplémentaire de modifier certaines parties de celui-ci. D’accord, cela semble un peu abstrait. En d’autres termes, le pattern est utile lorsque vous vous trouvez dans une situation telle que « J’adorerais utiliser cet algorithme mais j’ai besoin de changer quelques lignes pour qu’il fasse ce que je veux.

Prenons un exemple. Supposons que vous devez écrire une application bancaire en ligne simple. Les utilisateurs saisissent généralement un identifiant client, puis l’application récupère les informations du client dans la base de données de la banque et fait finalement quelque chose pour rendre le client heureux. Différentes applications bancaires en ligne pour différentes branches bancaires peuvent avoir différentes façons de rendre un client heureux (par exemple, en ajoutant un bonus sur leur compte ou simplement en leur envoyant moins de paperasse). Vous pouvez écrire la classe abstraite suivante pour représenter l’application bancaire en ligne :

La méthode processCustomer fournit un squelette pour l’algorithme : récupérer le client avec son identifiant, puis rendre le client heureux. Différentes branches peuvent maintenant fournir différentes implémentations de la méthode makeCustomerHappy en sous-classant la classe OnlineBanking.

Utiliser des expressions lambda

Vous pouvez aborder le même problème (en créant un aperçu d’un algorithme et en laissant les exécutants brancher certaines parties) en utilisant vos lambdas préférés. Les différents composants des algorithmes que vous souhaitez brancher peuvent être représentés par des expressions lambda ou des références de méthode.

Nous introduisons ici un deuxième argument à la méthode processCustomer de type Consumer <Customer> car il correspond à la signature de la méthode définie précédemment :

Vous pouvez maintenant connecter différents comportements directement sans heriter de la classe OnlineBanking en passant des expressions lambda :

8.2.3. Observateur

Le design pattern de l’observateur est une solution courante lorsqu’un objet (appelé sujet) doit notifier automatiquement une liste d’autres objets (appelés observateurs) lorsqu’un événement survient (par exemple, un changement d’état). Vous rencontrerez généralement ce modèle lorsque vous travailleerz avec des applications graphiques. Vous enregistrez un ensemble d’observateurs sur un composant GUI tel qu’un bouton. Si le bouton est cliqué, les observateurs sont avertis et peuvent exécuter une action spécifique. Mais ce design pattern n’est pas limité aux interfaces graphiques. Par exemple, il est également approprié dans une situation où plusieurs commerçants (observateurs) peuvent souhaiter réagir au changement de prix d’un stock (sujet). La figure 8.2 illustre le diagramme UML du modèle d’observateur.

Écrivons du code pour voir comment ce design pattern est utile dans la pratique. Vous concevez et implémentez un système de notification personnalisé pour une application telle que Twitter. Le concept est simple : plusieurs agences de presse (NY Times, The Guardian et Le Monde) sont abonnées à un flux de tweets de nouvelles et peuvent vouloir recevoir une notification si un tweet contient un mot-clé particulier.

D’abord, vous avez besoin d’une interface Observer qui regroupe les différents observateurs. Il y a juste une méthode appelée notify qui sera appelée par le sujet (Feed) lorsqu’un nouveau tweet est disponible :

Vous pouvez maintenant déclarer différents observateurs (ici, les trois journaux) qui produisent une action différente pour chaque mot-clé différent contenu dans un tweet :

Il vous manque toujours la partie cruciale: le sujet! Définissons lui une interface:

Le sujet peut enregistrer un nouvel observateur en utilisant la méthode registerObserver et informer ses observateurs d’un tweet avec la méthode notifyObservers. Allons de l’avant et implémentons la classe Feed :

C’est une implémentation assez simple : le flux conserve une liste interne d’observateurs qu’il peut ensuite notifier lorsqu’un tweet arrive. Vous pouvez maintenant créer une application de démonstration pour lier le sujet aux observateurs :

Sans surprise, The Guardian sera alerter de ce tweet!

Utiliser des expressions lambda

Vous vous demandez peut-être comment les expressions lambda peuvent être utiles au design pattern observateur. Notez que les différentes classes implémentant l’interface Observer fournissent toutes l’implémentation pour une seule méthode : notify. Ils encapsulent tous un morceau de code à exécuter quand un tweet arrivera. Les expressions lambda sont conçues spécifiquement pour supprimer ce code boiler plate. Au lieu d’instancier explicitement trois objets observateurs, vous pouvez passer directement une expression lambda pour représenter le comportement à exécuter :

Devriez-vous utiliser des expressions lambda tout le temps ? La réponse est non. Dans l’exemple que nous avons décrit, les expressions lambda fonctionnent très bien car le comportement à exécuter est simple, donc elles sont utiles pour supprimer le code standard. Mais les observateurs peuvent être plus complexes : ils peuvent avoir un état, définir plusieurs méthodes, etc. Dans ces situations, vous devriez garder l’utilisation de vos classes.

8.2.4. Chaîne de responsabilité

Le pattern chaîne de responsabilité est une solution courante pour créer une chaîne d’objets de traitement (telle qu’une chaîne d’opérations). Un objet de traitement peut faire un travail et transmettre le résultat à un autre objet, qui effectue ensuite un travail et le passe à un autre objet de traitement, et ainsi de suite.

Généralement, ce modèle est implémenté en définissant une classe abstraite représentant un objet de traitement qui définit un champ pour garder la trace d’un successeur. Une fois son travail terminé, l’objet de traitement transmet son travail à son successeur. Dans le code, il ressemble à ceci :

La figure 8.3 illustre le modèle de chaîne de responsabilité dans UML.

Ici, vous pouvez reconnaître le design pattern de méthodes template, dont nous avons discuté dans la section 8.2.2. La méthode handle fournit un aperçu de la façon de traiter le problème posé. Différents types d’objets de traitement peuvent être créés en héritant de la classe ProcessingObject et en fournissant une implémentation pour la méthode handleWork.

Regardons un exemple de la façon d’utiliser ce modèle. Vous pouvez créer deux objets de traitement en faisant du traitement de texte :

Vous pouvez maintenant connecter deux objets de traitement pour construire une chaîne d’opérations!

Utiliser des expressions lambda

Attendez une minute ! Ce pattern ressemble à des fonctions de chaînage (c’est-à-dire de composition). Nous avons discuté de la façon de composer des expressions lambda dans le chapitre 3. Vous pouvez représenter les objets de traitement comme une instance de Function <String, String> ou plus précisément un UnaryOperator <String>. Pour les chaîner, il suffit de composer ces fonctions en utilisant la méthode andThen.

8.2.5. Factory

Le design pattern Factory vous permet de créer des objets sans exposer la logique d’instanciation au client. Par exemple, disons que vous travaillez pour une banque et qu’ils ont besoin d’un moyen de créer différents produits financiers : des prêts, des obligations, des actions, etc.

Généralement, vous créez une classe Factory avec une méthode responsable de la création de différents objets, comme illustré ici :

Ici, Loan, Stock et Bond sont tous des sous-types de Produit. La méthode createProduct peut avoir une logique supplémentaire pour configurer chaque produit créé. Mais l’avantage est que vous pouvez maintenant créer ces objets sans exposer le constructeur et la configuration au client, ce qui simplifie la création de produits pour le client :

Utiliser des expressions lambda

Vous avez vu au chapitre 3 que vous pouvez vous référer aux constructeurs comme vous le faites pour les méthodes, en utilisant des références de méthode. Par exemple, voici comment faire référence au constructeur de Loan :

En utilisant cette technique, vous pouvez réécrire le code précédent en créant une Map qui mappe un nom de produit à son constructeur :

Vous pouvez maintenant utiliser cette Map pour instancier différents produits, comme vous l’avez fait avec le design pattern Factory :

C’est une bonne manière d’utiliser la fonction Java 8 pour atteindre le même but que le pattern factory. Mais cette technique ne s’adapte pas très bien si la méthode factory createProduct a besoin de prendre plusieurs arguments à passer aux constructeurs de produits. Vous devrez fournir une interface fonctionnelle différente de celle d’un simple Supplier.

Par exemple, supposons que vous souhaitiez stocker des constructeurs pour des produits qui prennent trois arguments (deux entiers et une String) ; vous aurez besoin de créer une interface fonctionnelle spéciale TriFunction pour gérer ce cas. Par conséquent, la signature de la Map devient plus complexe :

Vous avez vu comment écrire et refactoriser du code en utilisant des expressions lambda. Vous allez maintenant voir comment vous pouvez vous assurer que votre nouveau code est correct.

8.3. Test de lambdas

Vous avez maintenant saupoudré votre code avec des expressions lambda, et cela semble joli et concis. Mais dans la plupart des emplois de développeur, vous n’êtes pas payé pour avoir écrit du bon code mais pour avoir écrit du code correct.

Généralement, une bonne pratique d’ingénierie logicielle implique l’utilisation de tests unitaires pour s’assurer que votre programme se comporte comme prévu. Vous écrivez des cas de test, qui verifient que de petites parties individuelles de votre code source produisent les résultats attendus. Par exemple, considérons une classe Point simple pour une application graphique :

 

Le test unitaire suivant vérifie si la méthode moveRightBy se comporte comme prévu:

8.3.1. Test du comportement d’une lambda visible

Cela fonctionne bien car la méthode moveRightBy est publique. Par conséquent, elle peut être testée dans un cas de test. Mais les lambdas n’ont pas de nom (ce sont des fonctions anonymes, après tout), il est donc plus difficile de les tester dans votre code car vous ne pouvez pas les nommer de façon explicite (avec un nom).

Parfois, vous pouvez avoir accès à une lambda via un champ afin de pouvoir la réutiliser, et vous aimeriez vraiment tester la logique encapsulée dans ce lambda. Que pouvez-vous faire ? Vous pourriez tester l’expression lambda comme lorsque vous appelez des méthodes. Par exemple, supposons que vous ajoutez un champ static compareByXAndThenY dans la classe Point qui vous donne accès à un objet Comparator généré à partir de références de méthode :

Rappelez-vous que les expressions lambda génèrent une instance d’une interface fonctionnelle. Par conséquent, vous pouvez tester le comportement de cette instance. Ici, vous pouvez maintenant appeler la méthode compare sur l’objet Comparator compareByXAndThenY avec différents arguments pour tester que son comportement est celui escompté :

8.3.2. Mettre l’accent sur le comportement de la méthode utilisant une lambda

Mais le but des expressions lambdas est d’encapsuler un comportement unique à utiliser par une autre méthode. Dans ce cas, vous ne devez pas rendre publiques les expressions lambda ; elles ne sont qu’un détail d’implémentation. Au lieu de cela, nous soutenons que vous devriez tester le comportement de la méthode qui utilise une expression lambda. Par exemple, considérons la méthode moveAllPointsRightBy présentée ici :

Il ne sert à rien de tester la lambda p -> new Point (p.getX () + x, p.getY ()) ; ce n’est qu’un détail d’implémentation pour la méthode moveAllPointsRightBy. Au contraire, vous devriez vous concentrer sur le comportement de la méthode :

Notez que dans le test unitaire que vous venez de voir, il est important que la classe Point implémente la méthode equals de manière appropriée ; sinon, il s’appuiera sur l’implémentation par défaut de Object.

8.3.3. Tirer des lambdas complexes dans des méthodes séparées

Vous pouvez rencontrer une expression lambda très compliquée qui contient beaucoup de logique (par exemple, un algorithme de tarification). Que faites-vous, car vous ne pouvez pas vous référer à l’expression lambda dans votre test ? Une stratégie consiste à convertir l’expression lambda en une référence de méthode (il s’agit de déclarer une nouvelle méthode régulière), comme nous l’avons expliqué plus haut dans la section 8.1.3. Vous pouvez ensuite tester le comportement de la nouvelle méthode dans votre test comme vous le feriez avec n’importe quelle méthode normale.

8.3.4. Tester des fonctions de second dégré

Les méthodes qui prennent une fonction comme argument ou renvoient une autre fonction (les fonctions dites d’ordre supérieur, expliquées plus en détail au chapitre 14) sont un peu plus difficiles à traiter. Une chose que vous pouvez faire si une méthode prend un lambda comme argument est de tester son comportement avec différents lambda. Par exemple, vous pouvez tester la méthode de filtrage créée au chapitre 2 avec différents prédicats :

Que faire si la méthode à tester renvoie une autre fonction ? Vous pouvez tester le comportement de cette fonction en la traitant comme une instance d’une interface fonctionnelle, comme nous l’avons montré précédemment avec un comparateur.

Malheureusement, tout ne fonctionne pas la première fois, et vos tests peuvent signaler des erreurs liées à votre utilisation des expressions lambda. Nous passons maintenant au débogage.

8.4. Débogage

Dans l’arsenal d’un développeur, il existe deux armes principales de la vieille école pour déboguer le code problématique :

  • Examiner la stack trace
  • Logging

Les expressions lambda et les Streams peuvent apporter de nouveaux défis à votre routine de débogage typique. Nous explorons ceux-ci dans cette section.

8.4.1. Examiner la stacktrace

Lorsque votre programme s’est arrêté (par exemple, parce qu’une exception a été levée), la première chose que vous devez savoir, c’est l’endroit où il s’est arrêté et comment il est arrivé là. C’est là que les stack frames sont utiles. Chaque fois que votre programme effectue un appel de méthode, des informations sur l’appel sont générées, notamment l’emplacement de l’appel dans votre programme, les arguments de l’appel et les variables locales de la méthode appelée. Cette information est stockée sur une stack frame.

Lorsque votre programme échoue, vous obtenez une stack trace, qui est un résumé de la façon dont votre programme a atteint cet échec, stack frame par stack frame. En d’autres termes, vous obtenez une liste d’appels de méthode valables jusqu’à l’apparition de la panne. Cela vous aide à comprendre comment le problème s’est produit.

Lambdas et stack traces

Malheureusement, en raison du fait que les expressions lambda n’ont pas de noms, les stack traces peuvent être légèrement plus troublantes. Considérez le code simple suivant fait pour échouer volontairement :

L’exécuter produira une stack trace comme celle-ci:

Beuurk! Que s’est-il passer ? Bien sûr, le programme échouera, car le second élément de la liste des points est nul. Vous essayez ensuite de traiter une référence null. Étant donné que l’erreur se produit dans un pipeline de flux, la séquence entière des appels de méthode qui font fonctionner un pipeline de flux vous est exposé. Mais notez que la stack trace produit les lignes suivantes :

Cela signifie que l’erreur s’est produite dans une expression lambda. Malheureusement, parce que les expressions lambda n’ont pas de nom, le compilateur doit composer un nom pour s’y référer. Dans ce cas, c’est lambda$main$0, ce qui n’est pas très intuitif. Cela peut être problématique si vous avez de grandes classes contenant plusieurs expressions lambda.

Même si vous utilisez des références de méthode, il est toujours possible que la stack trace ne vous montre pas le nom de la méthode que vous avez utilisée. La modification du précédent lambda p -> p.getX () à la référence de méthode Point :: getX entraînera également une stack trace problématique :

Notez que si une référence de méthode fait référence à une méthode déclarée dans la même classe que celle où elle est utilisée, elle apparaîtra dans la trace de la pile. Par exemple, dans l’exemple suivant :

la méthode divideByZero est rapportée correctement dans la stack trace:

En général, gardez à l’esprit que les stack traces impliquant des expressions lambda peuvent être plus difficiles à comprendre. C’est un domaine où le compilateur peut être amélioré dans une future version de Java.

Supposons que vous essayez de déboguer un pipeline d’opérations dans un flux. Que pouvez-vous faire ? Imaginons que vous utilisez forEach pour imprimer ou consigner le résultat d’un flux comme suit :

 

Il produira la sortie suivante:

Malheureusement, une fois que vous appelez forEach, le flux entier est consommé. Ce qui serait vraiment utile, c’est de comprendre ce que chaque opération (Map, filtre, limite) produit dans le pipeline d’un flux.

C’est où la méthode de stream peek peut aider. Son but est d’exécuter une action sur chaque élément d’un flux dès qu’il est consommé. Mais il ne consomme pas tout le flux comme foreach. Il transmet l’élément sur lequel il a effectué une action à l’opération suivante dans le pipeline. La Figure 8.4 illustre l’opération peek.

Dans le code suivant, vous utilisez peek pour imprimer la valeur intermédiaire avant et après chaque opération dans le pipeline de flux:

Cela produira une sortie utile à chaque étape du pipeline:

8.5. Résumé

Voici les concepts clés que vous devriez retenir de ce chapitre :

  • Les expressions Lambda peuvent rendre votre code plus lisible et plus flexible.
  • Envisagez de convertir des classes anonymes en expressions lambda, mais méfiez-vous des différences sémantiques subtiles telles que la signification du mot-clé this et le shadowing des variables.
  • Les références de méthode peuvent rendre votre code plus lisible par rapport aux expressions lambda.
  • Envisagez de convertir le traitement de collection itératif pour utiliser l’API Streams.
  • Les expressions lambda peuvent aider à supprimer le code standard associé à plusieurs design pattern orientés objet tels que la stratégie, la méthode de template, le pattern observateur, la chaîne de responsabilité et la factory.
  • Les expressions lambda peuvent être testées unitairement, mais en général, vous devriez vous concentrer sur le comportement des méthodes où les expressions lambda apparaissent.
  • Envisagez d’extraire des expressions lambda complexes dans des méthodes régulières.
  • Les expressions lambda peuvent rendre les stack trace moins lisibles.
  • La méthode peek d’un flux est utile pour consigner les valeurs intermédiaires lorsqu’elles passent à certains points d’un pipeline de flux.