Développer un logiciel est génial, mais… je pense que nous pouvons tous convenir que cela peut être un peu une montagne russe émotionnelle. Au début, tout va bien. Vous ajoutez de nouvelles fonctionnalités les unes après les autres en quelques jours, voire quelques heures. Vous êtes sur une lancée!
Avancez rapidement 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 rapidement de quelques mois, et votre vitesse de développement diminue encore. Travailler sur ce projet n'est plus amusant et est devenu un frein.
Ça s'empire. Vous commencez à découvrir plusieurs bogues dans votre application. Souvent, résoudre un bug en crée deux nouveaux. À ce stade, vous pouvez commencer à chanter:
99 petits bugs dans le code. 99 petits bugs. Prenez-en un, corrigez-le autour,
… 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. Le développement de cette application est tout simplement pénible, car chaque modification du 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.
Alors, quelle est la raison de ce problème?
La cause principale est la complexité croissante. D'après mon expérience, le principal facteur de 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 donc? Parce que votre code d'enregistrement dépend du code qui envoie les e-mails. Désormais, vous ne pouvez rien changer sans introduire de bogues. Il n’est tout simplement pas possible de retracer toutes les dépendances.
Alors là vous l'avez; la vraie cause de nos problèmes est la complexité croissante provenant de toutes les dépendances de notre code.
Ce qui est drôle, c'est que ce problème est connu depuis des années maintenant. C'est un anti-motif commun appelé «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 exactement cet anti-modèle? Pour parler simplement, 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 laine), vous dessinez un cercle et placez les classes du projet de manière uniforme dessus. Tracez simplement une ligne entre chaque paire de classes qui dépendent les unes des autres. Vous pouvez maintenant voir la source de vos problèmes.
Alors je me suis posé une question: serait-il possible de réduire la complexité tout en s'amusant comme au début du projet? À vrai dire, vous ne pouvez pas éliminer tout de 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.
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'emboitera en fonction des normes de l'industrie telles que la taille des boulons et des décisions préalables telles que la taille des trous de montage et la distance entre eux.
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 l'appareil final comme prévu.
Pouvons-nous reproduire cela dans l'industrie du logiciel?
Bien sûr que nous pouvons! En utilisant des interfaces et l'inversion du principe de 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 est longue. Vous n'avez besoin d'aucun cadre sophistiqué pour appliquer cette méthode. Il vous suffit de vous en tenir à quelques règles simples et de rester discipliné.
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 les dépendances existantes et à les inverser à l’aide d’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 la mise en œuvre, ils peuvent travailler ensemble sans rien savoir l'un de l'autre. C'est brilliant.
Voyons sur un exemple simple comment découpler notre système pour créer un code modulaire. Les schémas ci-dessous ont été implémentés comme de simples applications Java. Vous pouvez les trouver sur ce Dépôt GitHub .
Supposons que nous ayons une application très simple constituée uniquement de Main
classe, trois services et un seul Util
classe. Ces éléments dépendent les uns des autres de multiples façons. Ci-dessous, vous pouvez voir une implémentation utilisant l'approche «grosse boule de boue». Les classes s'appellent simplement. Ils sont étroitement liés et vous ne pouvez pas simplement retirer un élément sans en toucher les autres. Les applications créées à l'aide de ce style vous permettent initialement de croître rapidement. Je pense que ce style est approprié pour les projets de validation de principe, car vous pouvez facilement jouer avec les choses. Néanmoins, il n’est pas approprié pour les solutions prêtes pour la production, car même la maintenance peut être dangereuse et tout changement unique peut créer des bogues imprévisibles. Le diagramme ci-dessous montre cette grande boule d'architecture de boue.
Dans une recherche d'une meilleure approche, nous pouvons utiliser une technique appelée injection de dépendances. Cette méthode suppose que tous les composants doivent être utilisés via des interfaces. J'ai lu des affirmations selon lesquelles cela découpait les éléments, mais est-ce vraiment le cas? Non. Regardez le diagramme ci-dessous.
La seule différence entre la situation actuelle et une grosse boule de boue est le fait que maintenant, au lieu d'appeler directement les classes, 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 supprimant 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 désordre dans un autre. Cela crée toujours le problème que si vous modifiez Service B
et Interface B
, vous devrez changer tous les éléments qui en dépendent. Cette approche ne résout rien; à mon avis, cela ajoute simplement une couche d'interface au-dessus des éléments. Vous ne devez jamais injecter de dépendances, mais vous devez plutôt vous en débarrasser une fois pour toutes. Vive l'indépendance!
L'approche, je crois, résout tous les principaux maux de tête des dépendances en n'utilisant pas du tout de dépendances. Vous créez un composant et son écouteur. Un auditeur est une interface simple. Chaque fois que vous devez appeler une méthode depuis l'extérieur de l'élément actuel, vous ajoutez simplement une méthode à l'écouteur et l'appelez à la place. L'élément est uniquement autorisé à utiliser des fichiers, à appeler des méthodes dans son package et à utiliser des classes fournies par le framework principal 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.
que signifie écrire du code
Veuillez noter que, dans cette architecture, seuls les Main
la classe a plusieurs dépendances. Il relie tous les éléments ensemble et encapsule la logique métier de l'application.
Les services, en revanche, sont des éléments totalement indépendants. Désormais, 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 jamais 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 professionnel ingénieur logiciel , ou un codeur pour la première fois compromis du pire code de spaghetti jamais cuisiné avec goto
déclarations mélangées. Cela n'a pas d'importance, car leur logique est encapsulée. Aussi horrible que cela puisse être, cela ne se répercutera jamais sur d'autres classes. Cela vous donne également le pouvoir de répartir 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 recommencer à écrire du code indépendant, comme au début de votre dernier projet.
Définissons le motif des éléments structurels 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 écouteur. Si vous souhaitez utiliser un élément, vous devez implémenter l'écouteur et effectuer des appels à la classe principale. Voici un diagramme de la configuration la plus simple:
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 d'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. Lorsqu'il s'agit de fichiers d'actifs tels que des images, des vues, des sons, etc., ils doivent é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 tour est joué!
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'il 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 d'importation. Existe-t-il des fichiers extérieurs à l'élément actuel? Si tel 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.
Jetons également un œil à un exemple simple 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. À l'appel 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 printOutput
méthode. 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 mise en œuvre. En raison de cette abstraction, cet élément peut être réutilisé facilement dans différentes applications.
Jetez maintenant un œil à la page principale App
classe. 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
Jetons un œil à l’utilisation du motif d’élément dans des applications à 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 avons initialement divisé les fichiers client et serveur. C’est une chose raisonnable à faire, car ils s’exécutent dans deux environnements différents: le navigateur et le serveur principal.
Ensuite, nous divisons le code de chaque couche en dossiers appelés application et éléments. 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 bien souvent réduite à de simples appels aux éléments.
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.
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 des éléments et utilise un modèle d'élément largement structurel.
À partir des faits saillants, vous pouvez voir que la page principale a été distinguée en tant qu'élément. Cette page comprend sa propre vue. Ainsi, lorsque, par exemple, vous souhaitez le réutiliser, vous pouvez simplement copier le dossier entier et le déposer dans un autre projet. Câblez simplement tout ensemble et vous êtes prêt.
C'est un exemple de base qui 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 le désordre du code sur lequel vous travaillez actuellement.
J'espère qu'avec ce nouvel ensemble d'outils, vous serez en mesure de développer plus facilement un code plus maintenable. Avant de vous lancer dans l'utilisation du motif d'élément dans la pratique, récapitulons rapidement tous les points principaux:
De nombreux problèmes logiciels surviennent en raison de dépendances entre plusieurs composants.
En apportant un changement à un endroit, vous pouvez introduire un comportement imprévisible ailleurs.
Trois approches architecturales courantes sont:
La grosse boule de boue. C’est parfait pour un développement rapide, mais pas pour une production stable.
Injection de dépendance. C’est une solution à moitié cuite que vous devez éviter.
un aperçu des points importants d'un plan d'affaires apparaissent dans le
Architecture des é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 a toutes les méthodes principales ainsi que d'un auditeur qui est une interface simple qui permet la communication avec le monde externe.
Afin d'obtenir une architecture d'élément full-stack, 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 des éléments se compose de tous les éléments indépendants, tandis que le dossier de l'application relie tout.
Vous pouvez maintenant commencer à créer et partager vos propres éléments. À long terme, cela vous aidera à créer des produits facilement maintenables. Bonne chance et dites-moi 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 compatriote ApeeScapeer Kevin Bloch.
En relation: Meilleures pratiques JS: créer un robot Discord avec TypeScript et injection de dépendancesLe code peut être difficile à maintenir en raison des dépendances entre plusieurs composants. En conséquence, apporter des changements à un endroit peut introduire un comportement imprévisible ailleurs.
L'architecture modulaire consiste à diviser une application en éléments indépendants. Nous reconnaissons toutes les dépendances inter-projets comme la cause de problèmes difficiles à trouver et à résoudre. L'indépendance totale rend ces composants extrêmement faciles à tester, entretenir, partager et réutiliser à l'avenir.
Les approches architecturales courantes sont: 1) La grosse boule de boue: idéale pour un développement rapide, mais pas si appropriée à des fins de production stable. 2) Injection de dépendance: une solution à moitié cuite à éviter. 3) Architecture d'élément: elle est maintenable et brillante pour les versions de production stables.