Cela fait six mois que j'ai commencé à travailler dans une entreprise qui développe une nouvelle application d'entreprise. Ma nouvelle équipe suit des pratiques agiles telles que la programmation en binôme et le développement piloté par les tests (TDD), et j'ai le sentiment honnête que le soleil brille pour nous !
Eh bien, presque.
Il y a des problèmes auxquels j'ai été confronté à la fois maintenant et dans des expériences industrielles passées sur le thème de la création d'applications d'entreprise solides que j'aimerais vous expliquer dans cet article.
En outre, je proposerai également une méthodologie simple basée sur les tests en premier pour créer des applications d'entreprise qui améliorent la communication d'équipe et favorisent des livraisons de code de haute qualité plus rapides.
Sans plus tarder, allons-y !
Les pratiques agiles sont très bénéfiques pour le prototypage rapide de logiciels. La TDD est au cœur de telles pratiques, offrant aux logiciels une propriété primordiale : la robustesse. L'écriture de tests en amont oblige les développeurs à réfléchir au comportement attendu et exceptionnel des composants logiciels qu'ils construisent, à améliorer la qualité du code et à garantir le respect des exigences fonctionnelles.
De plus, TDD est une pratique puissante qui permet aux développeurs de ne pas avoir peur de réparer, nettoyer et adapter leur code aux mises à jour des exigences fonctionnelles . Et c'est très bien. Cependant, il n'est pas si facile de mettre le TDD en pratique.
Au début de la construction de toute nouvelle application d'entreprise, nous (développeurs) rassemblons plusieurs exigences fonctionnelles. Ensuite, nous dérivons un ensemble de cas d'utilisation qui satisferont ces exigences fonctionnelles. Après cela, nous développons des composants logiciels situés à différentes couches, du plus haut au plus bas, en descendant jusqu'au cœur de l'application, c'est-à-dire son modèle de domaine. C'est la définition du processus de développement logiciel descendant .
Cependant, le processus de développement logiciel ascendant correspond mieux à TDD. Par rapport à l'alternative descendante, l'approche ascendante est une approche plus pragmatique, car elle nous permet, à nous développeurs, de couvrir progressivement tous les niveaux d'indirection en commençant par le plus basique (c'est-à-dire le modèle de domaine) et en progressant progressivement vers les couches supérieures de abstraction. Cela permet de produire des bases de code d'application plus solides, ce qui nous donne à son tour une grande confiance dans notre travail. Pour appliquer TDD dans une approche descendante, cependant, il faut d'abord écrire des tests pour les composants logiciels situés aux couches supérieures. De cette façon, les développeurs n'ont pas besoin de se moquer des dépendances des composants de la couche inférieure, car ils n'existent tout simplement pas encore.
La nécessité de créer de telles dépendances est déjà un problème car se moquer des composants de la couche inférieure n'est pas toujours possible ou, dans le meilleur des cas, semble contre-intuitif, par exemple, imaginez-vous devoir vous moquer de la logique d'un objet de domaine pour la secousse de produire un test des composants de service.
De plus, je doute personnellement que cela apporterait une quelconque valeur, car je pense que la validation des composants intermédiaires devrait toujours exercer toutes les dépendances sauf celles vis-à-vis des services externes (par exemple, une base de données), qui peuvent être moquées. Plus important encore, en raison de la complexité inhérente à la réalisation d'exigences fonctionnelles non triviales en tant que code, on ne comprend pas pleinement les implications techniques que certaines exigences fonctionnelles données ont sur le modèle de domaine jusqu'à ce que l'on commence à raisonner à leur sujet lors de la mise en œuvre du modèle de domaine.
Encore une fois, commencer à écrire des tests de composants logiciels intermédiaires n'apporte pas beaucoup de valeur, car bon nombre de ces tests (sinon tous) risquent d'être jetés à la poubelle une fois que les artefacts logiciels de la couche inférieure seront effectivement implémentés.
De plus, j'ai vu des développeurs de logiciels (en particulier des coéquipiers juniors) abandonner et mettre en œuvre une preuve de concept pour le cas d'utilisation en question sans écrire la moindre logique de validation. Cela utilise la pratique code-first, qui va à l'encontre de l'objectif de TDD. De plus, sans suivre les bonnes pratiques de livraison continue, il existe un risque élevé de pousser du code non validé vers notre référentiel de contrôle de version.
Alors, comment pourrions-nous appliquer efficacement TDD dans la production d'applications d'entreprise compte tenu d'un ensemble d'exigences fonctionnelles ?
Il y a beaucoup de littérature sur l'architecture hexagonale sur Internet. Je recommanderais particulièrement la lecture du livre blanc sur le sujet écrit par Alistair Cockburn.
Aux fins du présent article, permettez-moi de vous raconter une courte histoire réelle visant à expliquer brièvement la motivation et les principaux avantages de l'architecture hexagonale : Au cours des nombreuses années que j'ai développées pour les applications d'entreprise, j'ai vu beaucoup de gens ( moi-même inclus) en commençant de nouveaux projets en se concentrant sur d'autres sujets plutôt que sur notre véritable mission. Une telle mission consiste à apporter une valeur réelle aux entreprises pour lesquelles nous travaillons. Cette valeur est sur la logique de domaine de nos applications.
Paraphrasant Oncle Bob dans son livre Clean Architecture, tout le reste est une distraction, un détail de mise en œuvre qui peut (et doit) être reporté, idéalement à la fin du développement. Des exemples de détails d'implémentation sont les technologies de base de données, la logique du contrôleur ou la technologie frontale. Même le framework backend est un détail d'implémentation que nous pourrions choisir plus tard dans le processus de développement si nous le voulions vraiment. L'architecture hexagonale, également appelée ports et adaptateurs , est un modèle architectural visant à dissocier la logique centrale de l'application logicielle des détails d'implémentation externes.
Nous, les développeurs, devrions nous concentrer sur la logique de base des applications d'entreprise et reporter la mise en œuvre de la logique requise pour communiquer avec les services externes. Pour atteindre cet objectif, tout ce que nous avons vraiment besoin de faire est d'écrire des interfaces (appelées ports ) et de simuler les composants (appelés adaptateurs ) qui communiquent réellement avec les services externes. Ainsi, la mise en œuvre de l'adaptateur peut intervenir plus tard dans le processus de développement. Et plus ils arrivent tard, mieux c'est, car toutes les informations que nous obtenons pendant que nous produisons la logique de base s'avèrent vraiment utiles lors de la prise de décision sur les technologies à choisir.
L'image suivante est l'une des nombreuses illustrations existantes de l'architecture hexagonale :
Considérez les éléments qui composent l'hexagone intérieur. Outre le modèle de domaine, il existe également une couche de services d'application . Ces composants logiciels ne spécifient aucune logique de domaine. Au lieu de cela, ce sont de simples coordinateurs de la logique de l'adaptateur et du modèle de domaine. Un service d'application réalise un cas d'utilisation qui prend en charge un sous-ensemble d'exigences fonctionnelles pour l'application d'entreprise. Ce sont des données importantes à garder à l'esprit pour la suite.
Comme je l'ai dit plus tôt, l'application de TDD est plus facile en suivant le processus de développement logiciel ascendant. Cependant, beaucoup d'entre nous trouvent plus facile de raisonner sur la conception d'un système en suivant l'approche descendante. Et même s'il semble que nous encourons un conflit d'intérêts, c'est bien parce que nous pouvons commencer à concevoir (c'est-à-dire, dessiner en pseudocode ou un diagramme UML) nos services applicatifs de haut en bas sans écrire une seule ligne de code pour eux ; pas tant que nous n'aurons pas terminé la mise en œuvre du modèle de domaine.
Avant de commencer à coder, nous pourrions interpréter les services d'application comme des directives de conception de logiciels pour effectuer des tranches verticales des applications d'entreprise que nous devons créer. Chaque tranche verticale représente le chemin d'exécution complet d'un cas d'utilisation, depuis l'action effectuée par un service externe en amont ou un utilisateur dans une interface utilisateur jusqu'à toute opération exécutée sur un service externe en aval. Au moment où nous terminons la conception d'un service d'application, nous avons identifié les adaptateurs et les composants de domaine que nous devons implémenter. Maintenant que nous avons une visibilité sur le modèle de domaine, nous pouvons ensuite implémenter ses composants fondamentaux en appliquant TDD.
Ensuite, nous pouvons implémenter le service d'application en suivant l'approche test-first, en créant un port vers n'importe quel adaptateur de service externe et en simulant son implémentation réelle. Au moment où nous en avons terminé avec la mise en œuvre du service d'application et du modèle de domaine associé, nous pouvons vérifier que cette mise en œuvre est valide, c'est-à-dire sans bogue et qu'elle correspond à ses exigences fonctionnelles. Enfin, nous pouvons implémenter la logique de l'adaptateur, en appliquant également TDD.
Cette méthodologie permet une mise en œuvre rapide et progressive des applications d'entreprise, permettant aux développeurs de gagner en confiance dans la validité de tous les composants qu'ils créent sans jeter aucun test. De plus, il n'impose aucune restriction sur les mises à jour des exigences fonctionnelles.
Le diagramme de flux suivant décrit la méthodologie pour écrire la logique d'un cas d'utilisation. Notez que je ne parle pas de types de tests spécifiques car il s'agit d'un sujet assez controversé, même si je recommanderais de suivre les conventions et la terminologie utilisées dans l'article The Practical Test Pyramid .
La méthodologie proposée simplifie la répartition des tâches en équipe, qu'ils travaillent seuls ou en binôme. Certaines de ses étapes peuvent être effectuées en parallèle, par exemple, un coéquipier/une paire peut construire la logique de base en se moquant de toute référence à une dépendance externe, tandis qu'un autre peut travailler sur le développement du code d'intégration avec une telle dépendance externe, puisque ces deux parties sont découplé, raccourcissant ainsi le délai de livraison. Tout ce qu'ils ont à faire est de transmettre une API pour la dépendance externe réalisée en tant que port. La dépendance externe simulée et réelle doit implémenter l'interface de port susmentionnée. De même, une autre équipe/coéquipier/paire peut implémenter la partie frontale pour le cas d'utilisation, si c'est ce que l'application d'entreprise exige.
D'autre part, en raison de sa nature structurée, communiquer l'état de développement d'un cas d'utilisation concret entre pairs est facile. Lors du transfert d'un développement de cas d'utilisation, l'entité sortante peut simplement pointer le nouveau vers le service applicatif actuel qu'elle était en train de concevoir ou de mettre en œuvre. La nouvelle entité peut alors simplement tracer tout ce qui a déjà été créé sur l'adaptateur ou la logique de domaine dans la base de code.
Une note finale sur les détails de mise en œuvre : nous pourrions mettre à jour notre modèle de domaine pour spécifier certains de ces détails en écrivant des annotations/décorateurs de la technologie de base de données et des ressources de validation des données d'entrée à partir du cadre sous-jacent qui s'adapte le mieux à la nature de notre application. Je le déconseillerais cependant, car cela entraînerait une fuite des détails de mise en œuvre dans notre modèle de domaine, ce qui n'est pas la meilleure pratique puisque les détails de mise en œuvre et le modèle de domaine ont tendance à changer pour des raisons et à une fréquence différentes. D'un autre côté, comme l'explique Vaughn Vernon dans son livre Implementing Domain Driven Design (DDD), l'architecture hexagonale est étroitement liée à DDD. Cependant, vous n'avez pas besoin de suivre l'ensemble des pratiques et des modèles DDD pour créer des applications d'entreprise complexes basées sur l'architecture hexagonale, bien que je vous recommande vivement de le faire. Mais ces décisions, après tout, ne dépendent que de vous.
Le développement piloté par les tests est une pratique puissante dans la construction d'applications d'entreprise robustes. TDD est mieux appliqué en suivant le processus de développement logiciel ascendant. Cependant, comme les développeurs sont habitués à raisonner sur de telles applications selon une approche descendante, l'appliquer dans la pratique est difficile. Cet article expose une méthodologie simple pour aider les développeurs à appliquer efficacement TDD dans le développement d'applications d'entreprise.