Encore un article sur le packaging par features

Il existe moult pléthore d'articles, écrits par des personnes talentueuses et reconnues, sur le sujet du packaging by features mais j’ai quand même eu envie d’en écrire encore un :p ! J’ai eu envie de partager mon expérience de développeur Java passionné sur cette organisation qui me semble bien plus naturelle que celle que l’on trouve habituellement.

Le packaging by features est à opposer au packaging by layers ; on va donc simplement parler ici de la manière de ranger ses classes dans une application.

Why so serious?

Il n’est question que de la manière de ranger des fichiers dans des dossiers, quels impacts cela peut-il avoir ? Aussi étrange que cela puisse paraître cela va impacter :

  • La cohésion globale de l’application ;
  • La capacité à maîtriser les API internes de l’application ;
  • La facilité d’accueil de nouveaux développeurs ;
  • La capacité de l’application à accueillir sereinement les nouvelles features ;
  • Un éventuel découpage en plusieurs applications distinctes.

C’est en tout cas ce que je vais essayer de montrer dans les paragraphes suivants. Cependant, il ne faut pas me croire. Je vous invite à faire vos propres essais et à choisir la méthode qui vous convient le mieux.

Qu’est-ce que ça raconte ?

Le nommage des packages est le premier que l’on voit en ouvrant un projet. On nomme les éléments pour expliciter ce qu’ils font. Dans le cas des packages, ils doivent raconter ce que fait l’application. Il est très courant d’ouvrir une application et de trouver :

com.myapp
com.myapp.controllers
com.myapp.entities
com.myapp.repositories
com.myapp.services

Il faut bien admettre qu’on n’est que peu avancé sur les fonctionnalités de cette application en regardant ces packages. Ils nous apprennent essentiellement deux choses :

  • Cette application a les couches "classiques" des applications ;
  • Les auteurs nomment leurs packages au pluriel alors que c’est une mauvaise pratique.

En ouvrant cette même application on aurait pu trouver :

com.myapp
com.myapp.user
com.myapp.document
com.myapp.organisation

Même si les informations que présente cette simple structure ne sont pas incroyables, elles nous éclairent déjà davantage sur le métier de cette application.

Dans chacun de ces packages on va retrouver les layers qui apparaîtront alors dans le nom des classes (comme ils l’ont toujours fait). Chaque package pour une feature contiendra alors toutes les couches nécessaires à la fonctionnalité.

Le choix de la taille des features à exposer dépend d’un nombre important de facteurs, je suis d’avis d’exposer des composants autonomes et de limiter le nombre de points d'entrée dans le package. Ce découpage en features autonomes peut faciliter un éventuel découpage en plusieurs applications si le besoin vient à se faire sentir.

Qu’est-ce qui va changer ?

Un critère important pour le choix de toute architecture est de faciliter les changements des éléments qui vont effectivement changer dans le temps et de figer (et donc simplifier) les éléments qui ont le moins de chances de changer.

Un packaging by layers va, théoriquement, faciliter les changements de technologie transverses sur un des layers en permettant de faire les mêmes changements dans toutes les classes d’un même package.

Honnêtement, après avoir fait des DAO pendant des années, combien de fois vous ont-ils permis un changement de base de données sans souffrances ? Combien de fois avez-vous eu à faire ce changement ? On peut toujours arguer que cela vient du fait que, dans la grande majorité des cas, nos DAO ne sont en fait que des strategies et ne sont pas utilisés avec le reste du pattern. Mais ce serait en partie malhonnête :)

Un packaging by features va, théoriquement, faciliter les changements dans les features en permettant de faire les changements impactant une même feature dans les classes d’un même package. Au cours du dernier mois, combien de features avez-vous dû modifier, que ce soit pour apporter un changement sur une feature existante ou pour créer de nouvelles features ?

Il y a de fortes chances que vous ayez eu à faire plus de changements de features au cours du dernier mois que de changement de technologies sur tout un layer au cours de votre carrière de développeur (c’est en tout cas tout ce que je vous souhaite).

Très naturellement, comme les features vont vivre bien plus que les layers, on va très rapidement avoir plus de features que de layers (les applications ayant encore souvent plus de 5 features ou components).

Avec un packaging by layers cela va se traduire par au moins une class par feature dans chaque layer (on aura rapidement plusieurs dizaines de classes dans chaque package de layer).

Avec un packaging by feature on aura quelque classes par layer pour chaque feature se traduisant souvent par une dizaine de classes dans chaque package.

Que ce soit pour les changements dans l’existant ou pour l’ajout de nouvelles fonctionnalités, le packaging by features est donc bien plus proche des besoins de la grande majorité de nos applications.

Certes, ces avantages sont mineurs au quotidien avec nos IDE modernes, cependant, avoir des centaines de classes dans chaque dossier n’aide pas l’accueil de nouveaux développeurs qui ne connaissent pas encore les fonctionnalités de l’application. Le manque de vision organisée des fonctionnalités va rapidement entraîner la création de nouvelles versions des mêmes features dans le code avec tous les biais que peuvent entraîner la duplication de fonctionnalités dans des bases de code éparses.

Quelle cohésion pour quel couplage ?

Lorsqu’on construit des solutions, on cherche une forte cohésion et un couplage faible (high cohesion and low coupling). C’est-à-dire que les éléments d’un composant (quoi que l’on mette derrière ce mot) doivent être composés d’éléments fonctionnant de manière logique et fluide entre eux et que ce composant doit avoir un couplage faible avec les autres composants de la solution.

Considérons maintenant un package comme un composant, dans une organisation de package by layer la cohésion au sein de chaque package est faible (inexistante), les éléments d’un même package fonctionnant rarement ensemble. Le couplage entre les packages quant à lui est très fort puisque les classes d’un package ne peuvent fonctionner qu’avec les classes d’un autre package.

Avec une organisation de package by feature la cohésion est forte dans le package puisque les classes du package fonctionnent ensemble pour servir un même besoin. Le couplage, quant à lui, est faible puisqu’il se résume aux liens entre les features. Ce couplage va énormément changer en fonction du lien entre les features mais il restera toujours plus faible (et plus logique) qu’avec des packages by layers.

Cette cohésion forte et ce couplage faible vont, entre autres, faciliter les refactorings que ce soit au sein de chaque application ou même de la structure globale de la solution en faisant apparaître plus simplement les éléments qui peuvent être déplacés de manière autonome.

Quelles visibilités ?

Je suis développeur Java. Java a 4 visibilités, la grande oubliée : "package" est très injustement sous-estimée !

Cette visibilité permet de garder la logique de fonctionnement interne au sein d’un package. Il n’est en effet que rarement nécessaire d’exposer toutes nos classes, il est même commun de vouloir masquer une complexité interne pour n'exposer que des API mûrement réfléchies et reflétant un métier.

Le packaging by features permet d’utiliser cette visibilité pour masquer, par exemple, les respositories et les entities pour n’exposer que les services et des objets du domain. Ce sera tout simplement impossible avec le packaging by layers puisque tout devra être public pour pouvoir être utilisé.

Comment éviter les cycles ?

Une des problématiques du packaging by feature est la possible apparition de cycles entre les packages.

Le meilleur moyen d’éviter ces cycles est de construire nos packages autour de nos aggregates. Les aggregates étant construits pour limiter les transactions aux seules données de l’aggregate il sera alors possible de garder comme seuls points d’entrée des packages les controllers et services. Dans ce cas il est alors facile de gérer ces dépendances sous forme d’arbres en évitant tout cycle.

Dans certains cas cependant on va vouloir construire des aggregates regroupant plusieurs aggregates en se basant sur les fonctionnalités de notre ORM. Si on veut des liaisons bidirectionnelles entre les entities présentes dans deux packages nous aurons alors des dépendances cycliques. Dans beaucoup de cas il est possible d’éviter ces liaisons bidirectionnelles en ne mettant la liaison que dans le sens qui nous intéresse (les ORM actuels permettent de définir les relations de manière unidirectionnelle). On peut alors utiliser cette liaison pour les queries et passer par les services pour les command.

Dans certains cas il ne sera pas possible d’éviter les cycles entre les packages. Cependant, ces cycles et les problèmes qu’ils posent sont à mettre en regard avec les avantages qu’apporte cette méthode au quotidien.

Mais pourquoi trouve-t-on autant de packaging by layers ?

Les arguments en faveur du packaging by features me semblent sans appel, pourtant on trouve beaucoup plus de packaging by layers, voici quelques unes des raisons qui me viennent en tête :

  • La facilité : À la création d’une class, il est plus simple de la ranger avec les autres classes appartenant au même layer. En effet, quand on crée un StuffRepository il est très simple de savoir que c’est un repository. Savoir qu’il sert à la feature user (par exemple) demande plus de réflexion. Si cette nouvelle classe sert à plusieurs features cela sera d’autant plus simple car on aura pas à réfléchir à une éventuelle nouvelle organisation pour nos features. En fonction du contexte définir les features peut aussi être très (trop) complexe, dans ce cas le problème est certainement plus profond que simplement le découpage des packages ;
  • On pense technique : Quand on fabrique des solutions on a tendance à plus prendre en compte les choix techniques que les problématiques fonctionnelles. Cette mauvaise tendance nous amène à organiser nos solutions autours de ces problématiques techniques (et de la complexité accidentelle qu’elles entraînent) et non pas autours du métier. Je pense qu’elle est pour beaucoup dans le choix du packaging by layers. On cherche à voir une separation of concerns claire quand on ouvre nos applications plus qu’une liste de features ;
  • La reproduction des exemples : Beaucoup d’exemples et d’applications utilisant le packaging by layers on se contente souvent de reproduire ces exemples sans se poser d’avantage de questions sur ce point qui semble avoir bien peu d’impacts.

Il existe sûrement d’innombrables autres raisons pour le choix (ou le non choix) de ce type de packaging. Cependant, les différentes raisons me semblent court termistes dans le sens où elles facilitent certes la création des classes mais elles compliquent rapidement la maintenance. Cette maintenance, qui commence après le premier commit, représentant le plus clair de notre temps de développement, rendons-nous service et rangeons nos classes par responsabilités métier !