Depuis quelques années, j’écris des articles qui décrivent une nouvelle façon plus fonctionnelle d’écrire du code Java. Mais la question de savoir pourquoi nous devrions utiliser ce nouveau style de codage reste largement sans réponse. Cet article est une tentative de combler cette lacune.
Comme tout autre langage, Java évolue avec le temps. Il en va de même du style dans lequel le code Java est écrit. Le code écrit autour de l’an 2000 est très différent du code écrit après 2004-2006 lorsque Java5 puis Java6 ont été publiés. Les génériques et les annotations sont si répandus maintenant, qu’il est même difficile d’imaginer du code Java sans eux.
Puis vint Java8 avec les lambdas, Stream<T>
et Optional<T>
. Ces éléments fonctionnels devraient révolutionner le code Java, mais en grande partie ils ne le font pas. Dans un sens, ils ont définitivement affecté la façon dont nous écrivons du code Java, mais il n’y a pas eu de révolution. Évolution assez lente. Pourquoi? Essayons de trouver la réponse.
Je pense qu’il y avait deux raisons principales.
La première raison est que même les auteurs Java ressentaient une incertitude quant à la manière dont les nouveaux éléments fonctionnels s’intègrent dans l’écosystème Java existant. Pour voir cette incertitude, il suffit de lire Optional<T>
JavaDoc :
Remarque sur l’API : Facultatif est principalement destiné à être utilisé comme type de retour de méthode lorsqu’il est clairement nécessaire de représenter « aucun résultat » et lorsque l’utilisation de null est susceptible de provoquer des erreurs.
L’API montre également la même chose : la présence de get()
méthode (qui peut jeter NPE) ainsi que quelques orElseThrow()
Les méthodes sont des références claires au style de codage Java impératif traditionnel.
La deuxième raison est que le code Java existant, en particulier les bibliothèques et les frameworks, était incompatible avec les approches fonctionnelles – null
et les exceptions commerciales étaient le code Java idiomatique.
Avance rapide jusqu’à l’heure actuelle : Java 17 est sorti il y a quelques semaines, Java 11 est rapidement adopté à grande échelle, remplaçant Java 8, qui était omniprésent il y a quelques années. Pourtant, notre code ressemble presque à celui d’il y a 7 ans, lorsque Java 8 est sorti.
Cela vaut peut-être la peine de prendre du recul et de répondre à une autre question importante : devons-nous changer la façon dont nous écrivons du code Java ? Cela nous a assez bien servi pendant longtemps, nous avons des compétences, des guides, des bonnes pratiques et des tonnes de livres qui nous apprennent à écrire du code dans ce style. Avons-nous réellement besoin de changer cela?
Je pense que la réponse à cette question pourrait être dérivée de la réponse à une autre question : devons-nous améliorer les performances de développement ?
Je parie que nous le faisons. Les entreprises poussent les développeurs à fournir des applications plus rapidement. Idéalement, les projets sur lesquels nous travaillons devraient être écrits, testés et déployés avant même que l’entreprise ne réalise ce qui doit réellement être mis en œuvre. Je plaisante, bien sûr, mais la date de livraison « hier » est un rêve pour de nombreux hommes d’affaires.
Nous devons donc absolument améliorer les performances de développement. Chaque framework, IDE, méthodologie, approche de conception, etc., etc., se concentre sur l’amélioration de la vitesse à laquelle le logiciel (bien sûr, avec les normes de qualité nécessaires) est mis en œuvre et déployé. Néanmoins, malgré tout cela, il n’y a pas de percées visibles en termes de performances de développement.
Bien entendu, de nombreux éléments définissent le rythme de livraison des logiciels. Cet article se concentre uniquement sur les performances de développement.
De mon point de vue, la plupart des tentatives pour améliorer les performances de développement supposent qu’écrire moins de code (et moins de code en général) signifie automatiquement de meilleures performances. Bibliothèques et frameworks populaires comme Spring, Lombok, Feign – essayant tous de réduire la quantité de code. Même Kotlin a été créé avec une obsession de la brièveté par opposition à la « verbosité » Java. L’histoire a prouvé que cette hypothèse était fausse à plusieurs reprises (Perl et APL, peut-être, les exemples les plus notables), néanmoins, elle est toujours vivante et motive la plupart des efforts.
Tout développeur sait que l’écriture de code est une infime partie des activités de développement. La plupart du temps, nous sommes lecture de code. Lire moins de code est-il plus productif ? La première intention est de dire Oui, mais en pratique, la quantité de code et sa lisibilité sont à peine liées. La lecture et l’écriture du même code ont souvent une « impédance » différente sous forme de surcharge mentale.
Les meilleurs exemples de cette différence d’« impédance » sont probablement les expressions régulières. Les expressions régulières sont assez compactes et dans la plupart des cas assez faciles à écrire, notamment en utilisant d’innombrables outils dédiés. Mais la lecture d’expressions régulières est généralement pénible et prend beaucoup plus de temps. Pourquoi? La raison est la contexte perdu. Lorsque nous écrivons une expression régulière, nous connaissons le contexte : ce que nous voulons faire correspondre, quels cas doivent être pris en compte, à quoi peut ressembler une entrée, et ainsi de suite. L’expression elle-même est une représentation compressée de ce contexte. Mais lorsque nous les lisons, le contexte est perdu ou, pour être précis, compressé et compacté à l’aide d’une syntaxe très compacte. Une tentative de « décompresser » de l’expression régulière est une tâche assez longue. Dans certains cas, la réécriture à partir de zéro prend beaucoup moins de temps qu’une tentative de compréhension du code existant.
L’exemple ci-dessus donne un indice important : réduire la quantité de code n’a de sens que dans la mesure où le contexte reste préservé. Dès que la réduction du code entraîne une perte de contexte, cela commence à être contre-productif et nuit aux performances de développement.
Donc, si la taille du code n’est pas si pertinente, alors comment pouvons-nous vraiment améliorer la productivité ?
Évidemment, en préservant et/ou en restaurant le contexte perdu. Mais quand et pourquoi le contexte se perd ?
Mangeurs de contexte
Les mangeurs de contexte sont des pratiques ou des approches de codage qui entraînent une perte de contexte. Le code Java idiomatique a plusieurs de ces mangeurs de contexte. Les frameworks populaires ajoutent souvent leurs mangeurs de contexte. Jetons un coup d’œil aux deux mangeurs de contexte les plus omniprésents.
Variables Nullables
Oui, tu l’as lu correctement. Les variables Nullable masquent une partie du contexte – cas où la valeur de la variable peut être manquante. Regardez cet exemple de code :
String value = service.method(parameter);
Juste en regardant ce code, vous ne pouvez pas dire si value
peut être nul ou non. En d’autres termes, une partie du contexte est perdue. Pour le restaurer, il faut jeter un oeil dans le code service.method()
et l’analyser. La navigation vers cette méthode, la lecture de son code, le retour – tout cela est une distraction de la tâche en cours. Et le besoin constant de garder à l’esprit qu’une variable peut être null
, provoquer une surcharge mentale. Les développeurs expérimentés savent très bien garder de telles choses à l’esprit, mais cela ne signifie pas que cette surcharge mentale n’affecte pas leurs performances de développement.
Résumons :
Les variables nullables sont des mangeurs de contexte, des tueurs de performances de développement et une source d’erreurs d’exécution.
Exceptions
Idiomatic Java utilise des exceptions métier pour la propagation et la gestion des erreurs. Il existe deux types d’exceptions : cochées et non cochées. L’utilisation d’exceptions vérifiées est généralement déconseillée et souvent considérée comme un anti-modèle car elles provoquent un couplage de code profond. Bien que l’intention initiale de l’introduction d’exceptions vérifiées, soit dit en passant, était de préserver le contexte. Et le compilateur aide même à le préserver. Néanmoins, au fil du temps, nous sommes passés à des exceptions non contrôlées. Des exceptions non contrôlées ont été conçues pour le technique erreurs – accès à la variable nulle, tentative d’accès à la valeur en dehors des limites du tableau, etc.
Pensez-y un instant : nous utilisons technique exceptions non vérifiées pour Entreprise la gestion et la propagation des erreurs.
L’utilisation de la fonctionnalité de langage en dehors de la zone pour laquelle elle a été conçue entraîne une perte de contexte et des problèmes similaires à ceux décrits pour les variables nullables. Même les raisons sont les mêmes – les exceptions non vérifiées nécessitent la navigation et la lecture du code (souvent assez profondément dans la chaîne d’appel). Ils nécessitent également de basculer entre les tâches en cours et la gestion des erreurs. Et tout comme les variables nullables, les exceptions peuvent être une source d’erreurs d’exécution si elles ne sont pas traitées correctement.
Sommaire:
Les exceptions métier sont des mangeurs de contexte, des tueurs de performances de développement et une source de bogues.
Les frameworks comme mangeurs de contexte
Étant donné que les frameworks sont généralement spécifiques à un projet particulier, les problèmes qu’ils provoquent sont également spécifiques au projet. Néanmoins, si vous avez l’idée de la perte/préservation du contexte, vous remarquerez peut-être que les frameworks populaires comme Spring et d’autres, qui utilisent l’analyse des chemins de classe, l’idiome « convention sur la configuration » et d’autres « magie », suppriment intentionnellement une grande partie du contexte. et le remplacer par une connaissance implicite de la configuration par défaut (c’est-à-dire la surcharge mentale). Avec cette approche, l’application est divisée en un ensemble de classes vaguement liées. Sans support IDE, il est même difficile de naviguer entre les composants, tellement ils sont déconnectés. Outre la perte d’une grande partie du contexte, il existe un autre problème important, qui a un impact négatif sur la productivité : un nombre important d’erreurs sont déplacées de la compilation vers l’exécution. Les conséquences sont dévastatrices :
- D’autres tests sont nécessaires. Célèbre
contextLoads()
le test est un signe clair de ce problème. - Le support et la maintenance des logiciels nécessitent beaucoup plus de temps et d’efforts.
Ainsi, en réduisant la saisie de quelques lignes de code, nous obtenons beaucoup de maux de tête et une diminution des performances de développement. C’est le prix réel de la « magie »
Méthode Java fonctionnelle pragmatique
Le Java fonctionnel pragmatique est une tentative de résoudre certains problèmes mentionnés ci-dessus. Alors que l’intention initiale était simplement de préserver le contexte en encodant des états spéciaux dans des types de variables, l’utilisation pratique a montré un certain nombre d’autres avantages de l’approche adoptée :
- Navigation considérablement réduite.
- Un certain nombre d’erreurs sont déplacées de l’exécution vers la compilation, ce qui, à son tour, améliore la fiabilité et réduit le nombre de tests nécessaires.
- Suppression d’une partie importante des déclarations passe-partout et même des déclarations de type – moins de frappe, moins de code à lire, la logique métier est moins encombrée de détails techniques.
- Sensiblement moins de surcharge mentale et besoin de garder à l’esprit des éléments techniques non liés à la tâche en cours.