Créer du Code Vraiment Modulaire sans Dépendances

Développer un logiciel est génial, mais I je pense que nous pouvons tous convenir que cela peut être un peu une montagne russe émotionnelle. Au début, tout est génial. Vous ajoutez de nouvelles fonctionnalités l’une après l’autre en quelques jours, voire quelques heures. Vous êtes sur une lancée!

Avance rapide de quelques mois et votre vitesse de développement diminue. Est-ce parce que vous ne travaillez pas aussi dur qu’avant? Pas vraiment. Avançons quelques mois de plus et votre vitesse de développement diminue encore. Travailler sur ce projet n’est plus amusant et est devenu un frein.

Ça empire. Vous commencez à découvrir plusieurs bogues dans votre application. Souvent, la résolution d’un bogue en crée deux nouveaux. À ce stade, vous pouvez commencer à chanter:

99 petits bugs dans le code.99 petits insectes.Prenez-en un, corrigez-le,

1 127 petits bugs dans le code.

Que pensez-vous de travailler sur ce projet maintenant? Si vous êtes comme moi, vous commencez probablement à perdre votre motivation. C’est juste une douleur de développer cette application, car chaque changement de code existant peut avoir des conséquences imprévisibles.

Cette expérience est courante dans le monde du logiciel et peut expliquer pourquoi tant de programmeurs veulent jeter leur code source et tout réécrire.

Raisons pour lesquelles le développement logiciel Ralentit avec le temps

Alors, quelle est la raison de ce problème?

La cause principale est la complexité croissante. D’après mon expérience, le plus grand contributeur à la complexité globale est le fait que, dans la grande majorité des projets logiciels, tout est connecté. En raison des dépendances de chaque classe, si vous modifiez du code dans la classe qui envoie des e-mails, vos utilisateurs ne peuvent soudainement pas s’inscrire. Pourquoi ça ? Parce que votre code d’enregistrement dépend du code qui envoie les e-mails. Maintenant, vous ne pouvez rien changer sans introduire de bugs. Il n’est tout simplement pas possible de tracer toutes les dépendances.

Donc voilà; la vraie cause de nos problèmes est de complexifier la complexité provenant de toutes les dépendances de notre code.

Grosse boule de boue et Comment la réduire

Ce qui est drôle, c’est que ce problème est connu depuis des années maintenant. C’est un anti-modèle commun appelé la “grosse boule de boue.”J’ai vu ce type d’architecture dans presque tous les projets sur lesquels j’ai travaillé au fil des ans dans plusieurs entreprises différentes.

Alors, quel est cet anti-motif exactement? En termes simples, vous obtenez une grosse boule de boue lorsque chaque élément a une dépendance avec d’autres éléments. Ci-dessous, vous pouvez voir un graphique des dépendances du projet open-source bien connu Apache Hadoop. Afin de visualiser la grosse boule de boue (ou plutôt la grosse boule de fil), vous dessinez un cercle et placez les classes du projet uniformément dessus. Tracez simplement une ligne entre chaque paire de classes qui dépendent les unes des autres. Maintenant, vous pouvez voir la source de vos problèmes.

 Une visualisation de la grosse boule de boue d'Apache Hadoop, avec quelques dizaines de nœuds et des centaines de lignes les reliant les uns aux autres.

La “grosse boule de boue” d’Apache Hadoop

Une Solution avec un Code modulaire

Alors je me suis posé une question : Serait-il possible de réduire la complexité et de s’amuser encore comme au début du projet ? À vrai dire, vous ne pouvez pas éliminer toute la complexité. Si vous souhaitez ajouter de nouvelles fonctionnalités, vous devrez toujours augmenter la complexité du code. Néanmoins, la complexité peut être déplacée et séparée.

Comment Les Autres Industries Résolvent Ce Problème

Pensez à l’industrie mécanique. Lorsqu’un petit atelier de mécanique crée des machines, ils achètent un ensemble d’éléments standard, en créent quelques-uns personnalisés et les assemblent. Ils peuvent fabriquer ces composants complètement séparément et tout assembler à la fin, en ne faisant que quelques ajustements. Comment est-ce possible? Ils savent comment chaque élément s’emboîtera selon les normes de l’industrie, telles que la taille des boulons, et les décisions initiales, telles que la taille des trous de montage et la distance entre eux.

 Un schéma technique d'un mécanisme physique et comment ses pièces s'emboîtent. Les pièces sont numérotées dans l'ordre dans lequel attacher ensuite, mais cet ordre de gauche à droite va 5, 3, 4, 1, 2.

Chaque élément de l’assemblage ci-dessus peut être fourni par une société distincte qui n’a aucune connaissance du produit final ou de ses autres pièces. Tant que chaque élément modulaire est fabriqué selon les spécifications, vous pourrez créer le dispositif final comme prévu.

Pouvons-nous reproduire cela dans l’industrie du logiciel?

Bien sûr que nous pouvons! En utilisant des interfaces et le principe d’inversion du contrôle; la meilleure partie est le fait que cette approche peut être utilisée dans n’importe quel langage orienté objet: Java, C #, Swift, TypeScript, JavaScript, PHP – la liste s’allonge encore et encore. Vous n’avez besoin d’aucun cadre sophistiqué pour appliquer cette méthode. Il vous suffit de respecter quelques règles simples et de rester discipliné.

L’Inversion de Contrôle Est Votre Ami

Quand j’ai entendu parler pour la première fois de l’inversion de contrôle, j’ai immédiatement réalisé que j’avais trouvé une solution. C’est un concept qui consiste à prendre des dépendances existantes et à les inverser en utilisant des interfaces. Les interfaces sont de simples déclarations de méthodes. Ils ne fournissent aucune mise en œuvre concrète. En conséquence, ils peuvent être utilisés comme un accord entre deux éléments sur la façon de les connecter. Ils peuvent être utilisés comme connecteurs modulaires, si vous voulez. Tant qu’un élément fournit l’interface et qu’un autre élément en fournit l’implémentation, ils peuvent travailler ensemble sans rien savoir l’un de l’autre. C’est génial.

Voyons sur un exemple simple comment découpler notre système pour créer du code modulaire. Les diagrammes ci-dessous ont été implémentés sous forme d’applications Java simples. Vous pouvez les trouver sur ce dépôt GitHub.

Problème

Supposons que nous ayons une application très simple composée uniquement d’une classe Main, de trois services et d’une seule classe Util. Ces éléments dépendent les uns des autres de multiples façons. Ci-dessous, vous pouvez voir une implémentation utilisant l’approche “big ball of mud”. Les classes s’appellent simplement. Ils sont étroitement couplés, et vous ne pouvez pas simplement retirer un élément sans en toucher d’autres. Les applications créées à l’aide de ce style vous permettent d’abord de croître rapidement. Je crois que ce style est approprié pour les projets de preuve de concept car vous pouvez facilement jouer avec les choses. Néanmoins, ce n’est pas approprié pour les solutions prêtes à la production car même la maintenance peut être dangereuse et toute modification peut créer des bugs imprévisibles. Le diagramme ci-dessous montre cette grande architecture de boule de boue.

 Main utilise les services A, B et C, qui utilisent chacun Util. Le Service C utilise également le Service A.

Pourquoi L’injection de dépendance A tout faux

Dans une recherche d’une meilleure approche, nous pouvons utiliser une technique appelée injection de dépendance. Cette méthode suppose que tous les composants doivent être utilisés via des interfaces. J’ai lu des affirmations selon lesquelles il découple des éléments, mais est-ce vraiment le cas? Aucun. Regardez le diagramme ci-dessous.

 L'architecture précédente mais avec injection de dépendance. Maintenant, Main utilise les services d'interface A, B et C, qui sont implémentés par leurs services correspondants. Les services A et C utilisent tous deux le service d'interface B et l'Interface Util, qui est implémentée par Util. Le service C utilise également le Service d'interface A. Chaque service avec son interface est considéré comme un élément.

La seule différence entre la situation actuelle et une grosse boule de boue est le fait que maintenant, au lieu d’appeler les classes directement, nous les appelons via leurs interfaces. Il améliore légèrement la séparation des éléments les uns des autres. Si, par exemple, vous souhaitez réutiliser Service A dans un autre projet, vous pouvez le faire en retirant Service A lui-même, avec Interface A, ainsi que Interface B et Interface Util. Comme vous pouvez le voir, Service A dépend toujours d’autres éléments. En conséquence, nous avons toujours des problèmes avec le changement de code à un endroit et le comportement de gâchis à un autre. Cela crée toujours le problème que si vous modifiez Service B et Interface B, vous devrez modifier tous les éléments qui en dépendent. Cette approche ne résout rien; à mon avis, elle ajoute simplement une couche d’interface au-dessus des éléments. Vous ne devriez jamais injecter de dépendances, mais vous devriez plutôt vous en débarrasser une fois pour toutes. Hourra pour l’indépendance!

La Solution pour le code modulaire

L’approche que je crois résout tous les principaux maux de tête des dépendances le fait en n’utilisant pas du tout de dépendances. Vous créez un composant et son écouteur. Un écouteur est une interface simple. Chaque fois que vous avez besoin d’appeler une méthode depuis l’extérieur de l’élément courant, il vous suffit d’ajouter une méthode à l’écouteur et de l’appeler à la place. L’élément est uniquement autorisé à utiliser des fichiers, à appeler des méthodes dans son package et à utiliser des classes fournies par main framework ou d’autres bibliothèques utilisées. Ci-dessous, vous pouvez voir un diagramme de l’application modifiée pour utiliser l’architecture des éléments.

 Un schéma de l'application modifié pour utiliser l'architecture des éléments. Main utilise Util et les trois services. Main implémente également un écouteur pour chaque service, qui est utilisé par ce service. Un auditeur et un service ensemble sont considérés comme un élément.

Veuillez noter que, dans cette architecture, seule la classe Main a plusieurs dépendances. Il relie tous les éléments et encapsule la logique métier de l’application.

Les services, en revanche, sont des éléments complètement indépendants. Maintenant, vous pouvez retirer chaque service de cette application et les réutiliser ailleurs. Ils ne dépendent de rien d’autre. Mais attendez, ça va mieux: vous n’avez plus besoin de modifier ces services, tant que vous ne changez pas leur comportement. Tant que ces services font ce qu’ils sont censés faire, ils peuvent rester intacts jusqu’à la fin des temps. Ils peuvent être créés par un ingénieur logiciel professionnel, ou un codeur compromis pour la première fois du pire code spaghetti jamais préparé avec des instructions goto mélangées. Peu importe, car leur logique est encapsulée. Aussi horrible que cela puisse être, cela ne se répandra jamais dans d’autres classes. Cela vous donne également le pouvoir de diviser le travail dans un projet entre plusieurs développeurs, où chaque développeur peut travailler sur son propre composant indépendamment sans avoir besoin d’en interrompre un autre ou même de connaître l’existence d’autres développeurs.

Enfin, vous pouvez commencer à écrire du code indépendant une fois de plus, comme au début de votre dernier projet.

Motif d’élément

Définissons le motif d’élément structurel afin que nous puissions le créer de manière répétable.

La version la plus simple de l’élément se compose de deux choses: Une classe d’élément principal et un auditeur. Si vous souhaitez utiliser un élément, vous devez implémenter l’écouteur et passer des appels à la classe principale. Voici un schéma de la configuration la plus simple:

 Un diagramme d'un seul élément et de son écouteur dans une application. Comme précédemment, l'Application utilise l'élément, qui utilise son écouteur, qui est implémenté par l'Application.

De toute évidence, vous devrez éventuellement ajouter plus de complexité à l’élément, mais vous pouvez le faire facilement. Assurez-vous simplement qu’aucune de vos classes logiques ne dépend des autres fichiers du projet. Ils ne peuvent utiliser que le framework principal, les bibliothèques importées et les autres fichiers de cet élément. Quand il s’agit de fichiers d’actifs tels que des images, des vues, des sons, etc., ils devraient également être encapsulés dans des éléments afin qu’à l’avenir ils soient faciles à réutiliser. Vous pouvez simplement copier le dossier entier dans un autre projet et le voilà!

Ci-dessous, vous pouvez voir un exemple de graphique montrant un élément plus avancé. Notez qu’il s’agit d’une vue qu’il utilise et qu’elle ne dépend d’aucun autre fichier d’application. Si vous voulez connaître une méthode simple de vérification des dépendances, regardez simplement la section importer. Y a-t-il des fichiers en dehors de l’élément actuel? Si c’est le cas, vous devez supprimer ces dépendances en les déplaçant dans l’élément ou en ajoutant un appel approprié à l’écouteur.

 Un schéma simple d

Jetons également un coup d’œil à un simple exemple de “Hello World” créé en Java.

public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String args) { App app = new App(); app.start(); }}

Initialement, nous définissons ElementListener pour spécifier la méthode qui imprime la sortie. L’élément lui-même est défini ci-dessous. En appelant sayHello sur l’élément, il imprime simplement un message en utilisant ElementListener. Notez que l’élément est complètement indépendant de l’implémentation de la méthode printOutput. Il peut être imprimé dans la console, une imprimante physique ou une interface utilisateur sophistiquée. L’élément ne dépend pas de cette implémentation. Grâce à cette abstraction, cet élément peut être facilement réutilisé dans différentes applications.

Regardez maintenant la classe principale App. Il implémente l’auditeur et assemble l’élément avec une implémentation concrète. Maintenant, nous pouvons commencer à l’utiliser.

Vous pouvez également exécuter cet exemple en JavaScript ici

Architecture d’élément

Jetons un coup d’œil à l’utilisation du modèle d’élément dans une application à grande échelle. C’est une chose de le montrer dans un petit projet — c’en est une autre de l’appliquer au monde réel.

La structure d’une application Web full-stack que j’aime utiliser se présente comme suit:

src├── client│ ├── app│ └── elements│ └── server ├── app └── elements

Dans un dossier de code source, nous divisons initialement les fichiers client et serveur. C’est une chose raisonnable à faire, car ils fonctionnent dans deux environnements différents: le navigateur et le serveur principal.

Ensuite, nous divisons le code de chaque couche en dossiers appelés app et elements. Elements se compose de dossiers avec des composants indépendants, tandis que le dossier de l’application relie tous les éléments et stocke toute la logique métier.

De cette façon, les éléments peuvent être réutilisés entre différents projets, tandis que toute la complexité spécifique à l’application est encapsulée dans un seul dossier et souvent réduite à de simples appels aux éléments.

Exemple pratique

Croyant que la pratique l’emporte toujours sur la théorie, jetons un coup d’œil à un exemple réel créé dans Node.js et TypeScript.

Exemple réel

C’est une application Web très simple qui peut être utilisée comme point de départ pour des solutions plus avancées. Il suit l’architecture de l’élément et utilise un motif d’élément largement structurel.

Dans les faits saillants, vous pouvez voir que la page principale a été distinguée en tant qu’élément. Cette page inclut sa propre vue. Ainsi, lorsque, par exemple, vous souhaitez le réutiliser, vous pouvez simplement copier l’ensemble du dossier et le déposer dans un autre projet. Il suffit de tout câbler et vous êtes prêt.

C’est un exemple de base qui démontre que vous pouvez commencer à introduire des éléments dans votre propre application dès aujourd’hui. Vous pouvez commencer à distinguer les composants indépendants et séparer leur logique. Peu importe à quel point le code sur lequel vous travaillez actuellement est désordonné.

Développez Plus Vite, Réutilisez Plus Souvent!

J’espère qu’avec ce nouvel ensemble d’outils, vous pourrez développer plus facilement du code plus maintenable. Avant de vous lancer dans l’utilisation du modèle d’élément dans la pratique, récapitulons rapidement tous les points principaux:

  • Beaucoup de problèmes dans les logiciels se produisent à cause des dépendances entre plusieurs composants.

  • En faisant un changement à un endroit, vous pouvez introduire un comportement imprévisible ailleurs.

Trois approches architecturales communes sont:

  • La grosse boule de boue. C’est idéal pour un développement rapide, mais pas si idéal pour une production stable.

  • Injection de dépendance. C’est une solution à moitié cuite que vous devriez éviter.

  • Architecture d’éléments. Cette solution vous permet de créer des composants indépendants et de les réutiliser dans d’autres projets. Il est maintenable et brillant pour les versions de production stables.

Le modèle d’élément de base se compose d’une classe principale qui possède toutes les méthodes principales ainsi qu’un écouteur qui est une interface simple qui permet la communication avec le monde extérieur.

Afin d’obtenir une architecture d’éléments à pile complète, vous devez d’abord séparer votre front-end du code back-end. Ensuite, vous créez un dossier dans chacun pour une application et des éléments. Le dossier elements se compose de tous les éléments indépendants, tandis que le dossier app relie tout ensemble.

Maintenant, vous pouvez commencer à créer et à partager vos propres éléments. À long terme, cela vous aidera à créer des produits facilement maintenables. Bonne chance et faites-moi savoir ce que vous avez créé!

De plus, si vous optimisez prématurément votre code, lisez Comment éviter la Malédiction de l’optimisation prématurée par son collègue Toptaler Kevin Bloch.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.