Le développement de logiciels est génial, mais… je pense que nous pouvons tous convenir que cela peut être une montagne russe émotionnelle. Au début, tout va bien. Ajoutez de nouvelles fonctionnalités les unes après les autres en quelques jours, voire quelques heures. Vous avez de la chance!
Avancez rapidement de quelques mois et votre vitesse de développement ralentit. Est-ce parce que vous ne travaillez pas aussi dur qu'avant? Réelement non. Avancez encore quelques mois et votre vitesse de développement ralentira encore plus. Travailler sur ce projet n'est plus amusant et est devenu un frein.
Mais ça empire. Vous commencez à découvrir plusieurs erreurs dans votre application. Souvent, la résolution d'un bug en crée deux nouveaux. À ce stade, vous pouvez commencer à chanter:
99 petits bugs dans le code. 99 petites erreurs. Prends-en un, mets un patch dessus,
… 127 petites erreurs 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 compliqué, 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 abandonner 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 plus gros 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 un 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. Maintenant, vous ne pouvez rien changer sans introduire des erreurs. 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 l'augmentation de la complexité provenant de toutes les dépendances de notre code.
Le plus drôle, c'est que ce problème est connu depuis des années. C'est un anti-modèle commun appelé «grosse boule d'argile». 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 antipattern exactement? En termes simples, vous obtenez une grosse boule d'argile lorsque chaque élément dépend d'autres éléments. Ci-dessous, vous pouvez voir un graphique des dépendances du projet Apache Hadoop open source populaire. Pour visualiser la grosse boule d'argile (ou plutôt la grosse boule de fil), tracez un cercle et placez-y les classes du projet de manière égale. 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.
prix des aliments entiers vs walmart
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 supprimer Tout le monde la complexité. Si vous souhaitez ajouter de nouvelles fonctionnalités, vous devrez toujours augmenter la complexité de votre code. Cependant, la complexité peut bouger et se propager.
Pensez à l'industrie mécanique. Lorsqu'un petit atelier d'usinage crée des machines, il achète un ensemble d'articles standard, crée des articles personnalisés et les combine. Ils peuvent fabriquer ces composants complètement séparément et tout assembler en dernier, en ne faisant que quelques retouches. Comment est-ce possible? Ils savent comment chaque élément s'adaptera en fonction des normes de l'industrie telles que la taille des boulons et des décisions initiales telles que la taille du trou de montage et la distance entre les boulons.
Chaque article de l'ensemble ci-dessus peut être fourni par une société indépendante 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 pouvez créer le périphérique final comme prévu.
Pouvons-nous reproduire cela dans l'industrie du logiciel?
Nous pouvons certainement! En utilisant des interfaces et en inversant le 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 de prendre des dépendances existantes et de les inverser grâce à l'utilisation 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 le souhaitez. Tant qu'un élément fournit l'interface et qu'un autre élément fournit la mise en œuvre, ils peuvent travailler ensemble sans rien savoir l'un de l'autre. C'est génial.
Voyons dans un exemple simple comment nous pouvons découpler notre système pour créer du code modulaire. Les schémas suivants ont été implémentés en tant qu'applications Java simples. Vous pouvez les trouver dans ce Dépôt GitHub .
Supposons que nous ayons une application très simple composée d'une seule 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 «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 toucher les autres. Les applications créées dans ce style vous permettent initialement de vous développer rapidement. Je pense que ce style est approprié pour les projets de preuve de concept, car vous pouvez jouer facilement. Cependant, il ne convient pas aux solutions prêtes pour la production car même la maintenance peut être dangereuse et tout changement peut créer des erreurs imprévisibles. Le schéma ci-dessous montre cette grande boule d'architecture d'argile.
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 sur les interfaces. J'ai lu des affirmations selon lesquelles il détache des éléments, mais le fait-il vraiment? 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. Améliore légèrement les éléments de séparation les uns des autres. Si, par exemple, vous souhaitez réutiliser Servicio A
Dans un autre projet, vous pouvez le faire en supprimant Servicio A
, avec Interfaz A
, ainsi que Interfaz B
et Interface Útil
. Comme vous pouvez le voir, le Servicio A
cela dépend encore d'autres éléments. En conséquence, nous avons encore du mal à changer le code à un endroit et à gâcher le comportement à un autre. Cela crée toujours le problème que si vous modifiez Servicio B
e Interfaz 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, vous devez plutôt vous en débarrasser une fois pour toutes. Hourra pour l'indépendance!
L'approche qui, à mon avis, résout tous les problèmes majeurs de dépendance le fait 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 courant, ajoutez simplement une méthode à l'écouteur et appelez-la à 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.
Notez que, dans cette architecture, seule la classe Main
il a plusieurs dépendances. Connectez tous les éléments et encapsulez 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 la 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 pour la première fois engagé dans le pire code spaghetti que quiconque ait jamais concocté goto
mixte. Cela n'a pas d'importance, car votre logique est encapsulée. Aussi horrible soit-il, il ne se propagera jamais à d'autres classes. Cela vous donne également le pouvoir de diviser le travail sur 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 autonome, comme au début de votre dernier projet.
Définissons le motif de l'élément structurel afin de pouvoir le créer de manière répétée.
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:
Évidemment, vous devrez éventuellement ajouter plus de complexité à l'élément, mais vous pouvez facilement le faire. Assurez-vous simplement qu'aucune de vos classes logiques ne dépend d'autres fichiers du projet. Ils ne peuvent utiliser que le cadre principal, les bibliothèques importées et les autres fichiers de cet élément. En ce qui concerne les fichiers d'actifs tels que les images, les vues, les sons, etc., ils doivent également être encapsulés dans les éléments afin qu'ils soient faciles à réutiliser à l'avenir. 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 se compose d'une vue que vous utilisez et ne dépend d'aucun autre fichier d'application. Si vous voulez connaître une méthode simple pour vérifier les dépendances, regardez simplement la section d'importation. Y a-t-il un fichier extérieur à l'élément actuel? Si tel est le cas, vous devez supprimer ces dépendances en les déplaçant vers l'élément ou en ajoutant un appel approprié à l'écouteur.
Jetons 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. Appel sayHello
sur l'élément, imprimez 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é sur la console, une imprimante physique ou une interface utilisateur sophistiquée. L'élément ne dépend pas de cette implémentation. En raison de cette abstraction, cet élément peut être facilement réutilisé dans différentes applications.
Jetez maintenant un œil à la classe principale de App
. Implémentez l'auditeur et assemblez l'élément avec l'implémentation concrète. Maintenant, nous pouvons commencer à l'utiliser.
Vous pouvez également exécuter cet exemple en JavaScript ici
Jetons un coup d'œil à l'utilisation du modèle d'élément dans les applications à grande échelle. C'est une chose de le montrer dans un petit projet; une autre est de l'appliquer au monde réel.
La structure d'une application Web full-stack que j'aime utiliser ressemble à ceci:
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 fonctionnent dans deux environnements différents: le navigateur et le serveur principal.
Ensuite, nous divisons le code de chaque couche en dossiers appelés applications et éléments. Les éléments se composent de dossiers avec des composants séparés, tandis que le dossier d'application connecte tous les éléments et stocke toute la logique métier.
De cette manière, 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 d'éléments.
Si nous pensons 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. Connectez simplement tout et vous êtes prêt à partir.
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 pourrez plus facilement développer du code plus facile à maintenir. Avant de vous lancer dans l'utilisation du modèle d'élément dans la pratique, passons rapidement en revue tous les points principaux:
De nombreux problèmes surviennent dans les logiciels en raison de dépendances entre divers composants.
En apportant un changement à un endroit, vous pouvez introduire un comportement imprévisible à un autre endroit.
Les trois approches architecturales courantes sont:
La grande boule d'argile. Il est idéal pour un développement rapide, mais pas aussi bien pour une production stable.
Injection de dépendance. C'est une demi-solution que vous devriez éviter.
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 des 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.
Pour obtenir une architecture d'élément de pile complète, le front-end est d'abord séparé du code back-end. Ensuite, 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 autonomes, tandis que le dossier des applications connecte tout ensemble.
Vous pouvez maintenant commencer à créer et partager vos propres éléments. À long terme, cela vous aidera à créer des produits faciles à entretenir. 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_ de mon partenaire ApeeScape Kevin Bloch.