Erstellen von wirklich modularem Code ohne Abhängigkeiten
Softwareentwicklung ist großartig, aber … ich denke, wir können uns alle einig sein, dass es eine emotionale Achterbahnfahrt sein kann. Am Anfang ist alles großartig. Sie fügen nacheinander in wenigen Tagen, wenn nicht Stunden, neue Funktionen hinzu. Du bist auf einer Rolle!
Schneller Vorlauf ein paar Monate, und Ihre Entwicklungsgeschwindigkeit nimmt ab. Liegt es daran, dass Sie nicht mehr so hart arbeiten wie zuvor? Eigentlich nicht. Lassen Sie uns noch ein paar Monate vorspulen, und Ihre Entwicklungsgeschwindigkeit sinkt weiter. Die Arbeit an diesem Projekt macht keinen Spaß mehr und ist zu einer Belastung geworden.
Es wird schlimmer. Sie entdecken mehrere Fehler in Ihrer Anwendung. Wenn Sie einen Fehler lösen, werden häufig zwei neue erstellt. An diesem Punkt können Sie anfangen zu singen:
99 kleine Fehler im Code.99 kleine Käfer.Nehmen Sie einen herunter, patchen Sie ihn herum,
… 127 kleine Fehler im Code.
Wie fühlt es sich an, jetzt an diesem Projekt zu arbeiten? Wenn Sie wie ich sind, verlieren Sie wahrscheinlich Ihre Motivation. Die Entwicklung dieser Anwendung ist nur mühsam, da jede Änderung am vorhandenen Code unvorhersehbare Folgen haben kann.
Diese Erfahrung ist in der Software-Welt üblich und kann erklären, warum so viele Programmierer ihren Quellcode wegwerfen und alles neu schreiben wollen.
- Gründe, warum sich die Softwareentwicklung im Laufe der Zeit verlangsamt
- Großer Schlammball und wie man ihn reduziert
- Eine Lösung mit modularem Code
- Wie andere Branchen dieses Problem lösen
- Inversion of Control Is Your Friend
- Problem
- Warum Dependency Injection alles falsch gemacht hat
- Die Lösung für modularen Code
- Elementmuster
- Elementarchitektur
- Praktisches Beispiel
- Schneller entwickeln, öfter wiederverwenden!
Gründe, warum sich die Softwareentwicklung im Laufe der Zeit verlangsamt
Was ist der Grund für dieses Problem?
Die Hauptursache ist die steigende Komplexität. Aus meiner Erfahrung ist der größte Beitrag zur Gesamtkomplexität die Tatsache, dass in den allermeisten Softwareprojekten alles miteinander verbunden ist. Aufgrund der Abhängigkeiten, die jede Klasse hat, können sich Ihre Benutzer plötzlich nicht registrieren, wenn Sie Code in der Klasse ändern, die E-Mails sendet. Warum ist das so? Weil Ihr Registrierungscode von dem Code abhängt, der E-Mails sendet. Jetzt können Sie nichts mehr ändern, ohne Fehler einzuführen. Es ist einfach nicht möglich, alle Abhängigkeiten zu verfolgen.
Da haben Sie es also; Die wahre Ursache unserer Probleme ist die zunehmende Komplexität, die sich aus all den Abhängigkeiten ergibt, die unser Code hat.
Großer Schlammball und wie man ihn reduziert
Das Lustige ist, dieses Problem ist seit Jahren bekannt. Es ist ein übliches Anti-Muster, das als “großer Schlammball” bezeichnet wird.” Ich habe diese Art von Architektur in fast allen Projekten gesehen, an denen ich im Laufe der Jahre in verschiedenen Unternehmen gearbeitet habe.
Also, was ist dieses Anti-Muster genau? Einfach gesagt, Sie erhalten einen großen Schlammball, wenn jedes Element eine Abhängigkeit von anderen Elementen hat. Unten sehen Sie eine Grafik der Abhängigkeiten vom bekannten Open-Source-Projekt Apache Hadoop. Um den großen Schlammball (oder besser gesagt den großen Garnball) zu visualisieren, zeichnen Sie einen Kreis und platzieren Klassen aus dem Projekt gleichmäßig darauf. Zeichnen Sie einfach eine Linie zwischen den einzelnen Klassenpaaren, die voneinander abhängen. Jetzt können Sie die Ursache Ihrer Probleme sehen.
Eine Lösung mit modularem Code
Also stellte ich mir eine Frage: Wäre es möglich, die Komplexität zu reduzieren und trotzdem Spaß zu haben wie zu Beginn des Projekts? Um ehrlich zu sein, können Sie nicht die gesamte Komplexität beseitigen. Wenn Sie neue Funktionen hinzufügen möchten, müssen Sie immer die Codekomplexität erhöhen. Trotzdem kann Komplexität verschoben und getrennt werden.
Wie andere Branchen dieses Problem lösen
Denken Sie an die mechanische Industrie. Wenn ein kleines mechanisches Geschäft Maschinen herstellt, kaufen sie eine Reihe von Standardelementen, erstellen einige benutzerdefinierte und setzen sie zusammen. Sie können diese Komponenten komplett separat herstellen und am Ende alles zusammenbauen, indem sie nur ein paar Änderungen vornehmen. Wie ist das möglich? Sie wissen, wie jedes Element durch festgelegte Industriestandards wie Schraubengrößen und vorausschauende Entscheidungen wie die Größe der Befestigungslöcher und den Abstand zwischen ihnen zusammenpasst.
Jedes Element in der obigen Baugruppe kann von einer separaten Firma bereitgestellt werden, die keinerlei Kenntnisse über das Endprodukt oder seine anderen Teile hat. Solange jedes modulare Element gemäß den Spezifikationen hergestellt wird, können Sie das endgültige Gerät wie geplant erstellen.
Können wir das in der Softwareindustrie wiederholen?
Sicher können wir! Durch die Verwendung von Schnittstellen und Inversion des Steuerungsprinzips; Das Beste daran ist die Tatsache, dass dieser Ansatz in jeder objektorientierten Sprache verwendet werden kann: Java, C #, Swift, TypeScript, JavaScript, PHP – die Liste geht weiter und weiter. Sie benötigen kein ausgefallenes Framework, um diese Methode anzuwenden. Sie müssen sich nur an ein paar einfache Regeln halten und diszipliniert bleiben.
Inversion of Control Is Your Friend
Als ich zum ersten Mal von Inversion of control hörte, wurde mir sofort klar, dass ich eine Lösung gefunden hatte. Es ist ein Konzept, vorhandene Abhängigkeiten mithilfe von Schnittstellen zu invertieren. Schnittstellen sind einfache Deklarationen von Methoden. Sie bieten keine konkrete Implementierung. Infolgedessen können sie als Vereinbarung zwischen zwei Elementen verwendet werden, um sie zu verbinden. Sie können als modulare Steckverbinder verwendet werden, wenn man so will. Solange ein Element die Schnittstelle bereitstellt und ein anderes Element die Implementierung dafür bereitstellt, können sie zusammenarbeiten, ohne etwas voneinander zu wissen. Es ist brillant.
Sehen wir uns an einem einfachen Beispiel an, wie wir unser System entkoppeln können, um modularen Code zu erstellen. Die folgenden Diagramme wurden als einfache Java-Anwendungen implementiert. Sie finden sie in diesem GitHub-Repository.
Problem
Nehmen wir an, wir haben eine sehr einfache Anwendung, die nur aus einer Main
-Klasse, drei Diensten und einer einzelnen Util
-Klasse besteht. Diese Elemente hängen in mehrfacher Hinsicht voneinander ab. Unten sehen Sie eine Implementierung mit dem Ansatz “Big Ball of Mud”. Klassen rufen sich einfach gegenseitig an. Sie sind eng miteinander verbunden, und Sie können nicht einfach ein Element herausnehmen, ohne andere zu berühren. Mit diesem Stil erstellte Anwendungen können Sie zunächst schnell wachsen. Ich glaube, dieser Stil eignet sich für Proof-of-Concept-Projekte, da Sie leicht mit Dingen herumspielen können. Dennoch ist es nicht für produktionsreife Lösungen geeignet, da selbst die Wartung gefährlich sein kann und jede einzelne Änderung unvorhersehbare Fehler verursachen kann. Das folgende Diagramm zeigt diesen großen Ball der Schlammarchitektur.
Warum Dependency Injection alles falsch gemacht hat
Auf der Suche nach einem besseren Ansatz können wir eine Technik namens Dependency Injection verwenden. Bei dieser Methode wird davon ausgegangen, dass alle Komponenten über Schnittstellen verwendet werden sollen. Ich habe Behauptungen gelesen, dass es Elemente entkoppelt, aber tut es das wirklich? Nein. Schauen Sie sich das Diagramm unten an.
Der einzige Unterschied zwischen der aktuellen Situation und einem großen Schlammball ist die Tatsache, dass wir Klassen jetzt nicht direkt aufrufen, sondern über ihre Schnittstellen aufrufen. Es verbessert leicht die Trennung der Elemente voneinander. Wenn Sie beispielsweise Service A
in einem anderen Projekt wiederverwenden möchten, können Sie dies tun, indem Sie Service A
selbst zusammen mit Interface A
sowie Interface B
und Interface Util
entfernen. Wie Sie sehen können, hängt Service A
immer noch von anderen Elementen ab. Infolgedessen haben wir immer noch Probleme, Code an einer Stelle zu ändern und das Verhalten an einer anderen Stelle zu vermasseln. Es entsteht immer noch das Problem, dass Sie beim Ändern von Service B
und Interface B
alle davon abhängigen Elemente ändern müssen. Dieser Ansatz löst nichts; Meiner Meinung nach fügt er nur eine Schnittstellenebene über den Elementen hinzu. Sie sollten niemals Abhängigkeiten injizieren, sondern sie ein für alle Mal loswerden. Hurra zur Unabhängigkeit!
Die Lösung für modularen Code
Der Ansatz, von dem ich glaube, dass er alle Hauptprobleme von Abhängigkeiten löst, tut dies, indem er überhaupt keine Abhängigkeiten verwendet. Sie erstellen eine Komponente und ihren Listener. Ein Listener ist eine einfache Schnittstelle. Wenn Sie eine Methode von außerhalb des aktuellen Elements aufrufen müssen, fügen Sie dem Listener einfach eine Methode hinzu und rufen sie stattdessen auf. Das Element darf nur Dateien verwenden, Methoden innerhalb seines Pakets aufrufen und Klassen verwenden, die vom main Framework oder anderen verwendeten Bibliotheken bereitgestellt werden. Unten sehen Sie ein Diagramm der Anwendung, die geändert wurde, um die Elementarchitektur zu verwenden.
Bitte beachten Sie, dass in dieser Architektur nur die Klasse Main
mehrere Abhängigkeiten aufweist. Es verbindet alle Elemente miteinander und kapselt die Geschäftslogik der Anwendung ein.
Dienste hingegen sind völlig unabhängige Elemente. Jetzt können Sie jeden Dienst aus dieser Anwendung herausnehmen und an anderer Stelle wiederverwenden. Sie sind von nichts anderem abhängig. Aber warten Sie, es wird besser: Sie müssen diese Dienste nie wieder ändern, solange Sie ihr Verhalten nicht ändern. Solange diese Dienste tun, was sie tun sollen, können sie bis zum Ende der Zeit unberührt bleiben. Sie können von einem professionellen Softwareentwickler oder einem Erstcodierer erstellt werden, der den schlimmsten Spaghetti-Code kompromittiert, den jemals jemand mit goto
-Anweisungen gekocht hat. Es spielt keine Rolle, weil ihre Logik gekapselt ist. So schrecklich es auch sein mag, es wird niemals auf andere Klassen übergreifen. Dadurch haben Sie auch die Möglichkeit, die Arbeit in einem Projekt auf mehrere Entwickler aufzuteilen, wobei jeder Entwickler unabhängig an seiner eigenen Komponente arbeiten kann, ohne einen anderen unterbrechen zu müssen oder sogar über die Existenz anderer Entwickler Bescheid zu wissen.
Schließlich können Sie noch einmal unabhängigen Code schreiben, genau wie zu Beginn Ihres letzten Projekts.
Elementmuster
Definieren wir das Strukturelementmuster, damit wir es wiederholbar erstellen können.
Die einfachste Version des Elements besteht aus zwei Dingen: Eine Hauptelementklasse und ein Listener. Wenn Sie ein Element verwenden möchten, müssen Sie den Listener implementieren und die Hauptklasse aufrufen. Hier ist ein Diagramm der einfachsten Konfiguration:
Natürlich müssen Sie dem Element irgendwann mehr Komplexität hinzufügen, aber Sie können dies problemlos tun. Stellen Sie einfach sicher, dass keine Ihrer Logikklassen von anderen Dateien im Projekt abhängt. Sie können nur das Hauptframework, importierte Bibliotheken und andere Dateien in diesem Element verwenden. Wenn es um Asset-Dateien wie Bilder, Ansichten, Sounds usw. geht., sie sollten auch innerhalb von Elementen gekapselt werden, damit sie in Zukunft leicht wiederverwendet werden können. Sie können einfach den gesamten Ordner in ein anderes Projekt kopieren und da ist es!
Unten sehen Sie ein Beispieldiagramm, das ein fortgeschritteneres Element zeigt. Beachten Sie, dass es aus einer Ansicht besteht, die es verwendet, und nicht von anderen Anwendungsdateien abhängt. Wenn Sie eine einfache Methode zum Überprüfen von Abhängigkeiten kennen möchten, lesen Sie einfach den Abschnitt Importieren. Gibt es Dateien von außerhalb des aktuellen Elements? Wenn ja, müssen Sie diese Abhängigkeiten entfernen, indem Sie sie entweder in das Element verschieben oder dem Listener einen entsprechenden Aufruf hinzufügen.
Schauen wir uns auch ein einfaches “Hello World” -Beispiel an, das in Java erstellt wurde.
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(); }}
Zunächst definieren wir ElementListener
, um die Methode anzugeben, mit der die Ausgabe gedruckt wird. Das Element selbst wird unten definiert. Beim Aufruf von sayHello
für das Element wird einfach eine Nachricht mit ElementListener
gedruckt. Beachten Sie, dass das Element völlig unabhängig von der Implementierung der Methode printOutput
ist. Es kann in die Konsole, einen physischen Drucker oder eine ausgefallene Benutzeroberfläche gedruckt werden. Das Element hängt nicht von dieser Implementierung ab. Aufgrund dieser Abstraktion kann dieses Element problemlos in verschiedenen Anwendungen wiederverwendet werden.
Sehen Sie sich nun die Hauptklasse App
an. Es implementiert den Listener und assembliert das Element zusammen mit der konkreten Implementierung. Jetzt können wir anfangen, es zu benutzen.
Sie können dieses Beispiel auch hier in JavaScript ausführen
Elementarchitektur
Werfen wir einen Blick auf die Verwendung des Elementmusters in großen Anwendungen. Es ist eine Sache, es in einem kleinen Projekt zu zeigen – es ist eine andere, es auf die reale Welt anzuwenden.
Die Struktur einer Full-Stack-Webanwendung, die ich gerne verwende, sieht wie folgt aus:
src├── client│ ├── app│ └── elements│ └── server ├── app └── elements
In einem Quellcodeordner teilen wir zunächst die Client- und Serverdateien auf. Es ist eine vernünftige Sache zu tun, da sie in zwei verschiedenen Umgebungen ausgeführt werden: dem Browser und dem Back-End-Server.
Dann teilen wir den Code in jeder Ebene in Ordner namens app und elements . Elements besteht aus Ordnern mit unabhängigen Komponenten, während der App-Ordner alle Elemente miteinander verbindet und die gesamte Geschäftslogik speichert.
Auf diese Weise können Elemente zwischen verschiedenen Projekten wiederverwendet werden, während die gesamte anwendungsspezifische Komplexität in einem einzigen Ordner gekapselt und häufig auf einfache Aufrufe von Elementen reduziert wird.
Praktisches Beispiel
Da wir glauben, dass die Praxis immer die Theorie übertrifft, werfen wir einen Blick auf ein reales Beispiel, das in Node erstellt wurde.js und TypeScript.
Beispiel aus der Praxis
Es ist eine sehr einfache Webanwendung, die als Ausgangspunkt für fortschrittlichere Lösungen verwendet werden kann. Es folgt der Elementarchitektur und verwendet ein weitgehend strukturelles Elementmuster.
Anhand der Hervorhebungen können Sie sehen, dass die Hauptseite als Element unterschieden wurde. Diese Seite enthält eine eigene Ansicht. Wenn Sie es beispielsweise wiederverwenden möchten, können Sie einfach den gesamten Ordner kopieren und in ein anderes Projekt ablegen. Verdrahten Sie einfach alles zusammen und Sie sind fertig.
Dies ist ein grundlegendes Beispiel, das zeigt, dass Sie heute mit der Einführung von Elementen in Ihre eigene Anwendung beginnen können. Sie können damit beginnen, unabhängige Komponenten zu unterscheiden und ihre Logik zu trennen. Es spielt keine Rolle, wie chaotisch der Code ist, an dem Sie gerade arbeiten.
Schneller entwickeln, öfter wiederverwenden!
Ich hoffe, dass Sie mit diesen neuen Tools leichter Code entwickeln können, der wartbarer ist. Bevor Sie das Elementmuster in der Praxis verwenden, lassen Sie uns kurz alle wichtigen Punkte zusammenfassen:
-
Viele Probleme in der Software treten aufgrund von Abhängigkeiten zwischen mehreren Komponenten auf.
-
Wenn Sie an einer Stelle eine Änderung vornehmen, können Sie an einer anderen Stelle unvorhersehbares Verhalten einführen.
Drei gemeinsame architektonische Ansätze sind:
-
Der große Schlammball. Es ist großartig für die schnelle Entwicklung, aber nicht so toll für stabile Produktionszwecke.
-
Abhängigkeitsinjektion. Es ist eine halbfertige Lösung, die Sie vermeiden sollten.
-
Element Architektur. Mit dieser Lösung können Sie unabhängige Komponenten erstellen und in anderen Projekten wiederverwenden. Es ist wartbar und brillant für stabile Produktionsversionen.
Das Grundelementmuster besteht aus einer Hauptklasse mit allen wichtigen Methoden sowie einem Listener, einer einfachen Schnittstelle, die die Kommunikation mit der Außenwelt ermöglicht.
Um eine Full-Stack-Elementarchitektur zu erreichen, trennen Sie zuerst Ihr Front-End vom Back-End-Code. Dann erstellen Sie jeweils einen Ordner für eine App und Elemente. Der Elementordner besteht aus allen unabhängigen Elementen, während der App-Ordner alles miteinander verbindet.
Jetzt können Sie Ihre eigenen Elemente erstellen und freigeben. Auf lange Sicht wird es Ihnen helfen, leicht wartbare Produkte zu erstellen. Viel Glück und lass mich wissen, was du geschaffen hast!
Wenn Sie Ihren Code vorzeitig optimieren, lesen Sie, wie Sie den Fluch der vorzeitigen Optimierung von Kevin Bloch vermeiden können.