paint-brush
Testen einer sauberen Architektur in einer Frontend-Anwendung – ist das sinnvoll?von@playerony
10,637 Lesungen
10,637 Lesungen

Testen einer sauberen Architektur in einer Frontend-Anwendung – ist das sinnvoll?

von Paweł Wojtasiński21m2023/05/01
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Frontend-Entwickler stehen vor der Herausforderung, skalierbare und wartbare Architekturen zu erstellen. Viele der vorgeschlagenen Architekturideen wurden möglicherweise nie in realen Produktionsumgebungen umgesetzt. Ziel dieses Artikels ist es, Frontend-Entwicklern die Werkzeuge an die Hand zu geben, die sie benötigen, um sich in der sich ständig weiterentwickelnden Welt der Website-Entwicklung zurechtzufinden.
featured image - Testen einer sauberen Architektur in einer Frontend-Anwendung – ist das sinnvoll?
Paweł Wojtasiński HackerNoon profile picture

Mit der Weiterentwicklung der digitalen Landschaft nimmt auch die Komplexität moderner Websites zu. Angesichts der steigenden Nachfrage nach einer besseren Benutzererfahrung und erweiterten Funktionen stehen Frontend-Entwickler vor der Herausforderung, skalierbare, wartbare und effiziente Architekturen zu erstellen.


Unter den zahlreichen Artikeln und Ressourcen, die zum Thema Frontend-Architektur verfügbar sind, konzentriert sich ein erheblicher Teil auf Clean Architecture und deren Anpassung. Tatsächlich diskutieren mehr als 50 % der fast 70 befragten Artikel Clean Architecture im Kontext der Front-End-Entwicklung.


Trotz der Fülle an Informationen bleibt ein eklatantes Problem bestehen: Viele der vorgeschlagenen Architekturideen wurden möglicherweise nie in realen Produktionsumgebungen umgesetzt. Dies lässt Zweifel an ihrer Wirksamkeit und Anwendbarkeit in praktischen Szenarien aufkommen.


Angetrieben von dieser Sorge begab ich mich auf eine sechsmonatige Reise zur Implementierung von Clean Architecture im Frontend, die es mir ermöglichte, mich mit der Realität dieser Ideen auseinanderzusetzen und die Spreu vom Weizen zu trennen.


In diesem Artikel teile ich meine Erfahrungen und Erkenntnisse aus dieser Reise und biete einen umfassenden Leitfaden zur erfolgreichen Implementierung von Clean Architecture im Frontend.


Dieser Artikel beleuchtet die Herausforderungen, Best Practices und realen Lösungen und zielt darauf ab, Frontend-Entwicklern die Tools an die Hand zu geben, die sie benötigen, um sich in der sich ständig weiterentwickelnden Welt der Website-Entwicklung zurechtzufinden.

Rahmenwerke

Im heutigen sich schnell entwickelnden digitalen Ökosystem haben Entwickler die Qual der Wahl, wenn es um Frontend-Frameworks geht. Diese Fülle an Möglichkeiten beseitigt zahlreiche Probleme und vereinfacht den Entwicklungsprozess.


Dies führt jedoch auch zu endlosen Debatten unter Entwicklern, von denen jeder behauptet, dass sein bevorzugtes Framework anderen überlegen sei. Die Wahrheit ist, dass in unserer schnelllebigen Welt täglich neue JavaScript-Bibliotheken entstehen und Frameworks fast monatlich eingeführt werden.


Um in einem solch dynamischen Umfeld Flexibilität und Anpassungsfähigkeit aufrechtzuerhalten, benötigen wir eine Architektur, die über bestimmte Frameworks und Technologien hinausgeht.


Dies ist besonders wichtig für Produktunternehmen oder langfristige Verträge, die Wartung beinhalten und bei denen sich ändernde Trends und technologische Fortschritte berücksichtigt werden müssen.


Durch die Unabhängigkeit von Details wie Frameworks können wir uns auf das Produkt konzentrieren, an dem wir arbeiten, und uns auf Änderungen vorbereiten, die während seines Lebenszyklus auftreten können.


Keine Angst; Dieser Artikel soll eine Antwort auf dieses Dilemma geben.

Fullstack-Teamkooperation

Bei meinem Bestreben, die Clean Architecture im Frontend zu implementieren, habe ich eng mit mehreren Fullstack- und Backend-Entwicklern zusammengearbeitet, um sicherzustellen, dass die Architektur auch für diejenigen mit minimaler Frontend-Erfahrung verständlich und wartbar ist.


Daher ist eine der Hauptanforderungen unserer Architektur die Zugänglichkeit für Backend-Entwickler, die sich möglicherweise nicht mit den Feinheiten des Frontends auskennen, sowie für Fullstack-Entwickler, die möglicherweise nicht über umfassende Frontend-Kenntnisse verfügen.


Durch die Förderung einer nahtlosen Zusammenarbeit zwischen Frontend- und Backend-Teams zielt die Architektur darauf ab, diese Lücke zu schließen und ein einheitliches Entwicklungserlebnis zu schaffen.

Theoretische Grundlagen

Um einige großartige Dinge zu bauen, müssen wir uns leider etwas Hintergrundwissen aneignen. Ein klares Verständnis der zugrunde liegenden Prinzipien erleichtert nicht nur den Implementierungsprozess, sondern stellt auch sicher, dass die Architektur den Best Practices der Softwareentwicklung entspricht.


In diesem Abschnitt stellen wir drei Schlüsselkonzepte vor, die die Grundlage unseres Architekturansatzes bilden: SOLID-Prinzipien , Clean Architecture (die eigentlich aus SOLID-Prinzipien hervorgeht) und Atomic Design . Wenn Ihnen diese Bereiche am Herzen liegen, können Sie diesen Abschnitt überspringen.

SOLIDE Prinzipien

SOLID ist ein Akronym, das fünf Designprinzipien darstellt, die Entwickler bei der Erstellung skalierbarer, wartbarer und modularer Software unterstützen:


  • Single-Responsibility-Prinzip (SRP) : Dieses Prinzip besagt, dass eine Klasse nur einen Grund für Änderungen haben sollte, was bedeutet, dass sie eine einzige Verantwortung haben sollte. Durch die Einhaltung von SRP können Entwickler fokussierteren, wartbareren und testbareren Code erstellen.


  • Open/Closed-Prinzip (OCP) : Laut OCP sollten Softwareeinheiten für Erweiterungen offen, für Änderungen jedoch geschlossen sein. Dies bedeutet, dass Entwickler in der Lage sein sollten, neue Funktionen hinzuzufügen, ohne den vorhandenen Code zu ändern, wodurch das Risiko der Einführung von Fehlern verringert wird.


  • Liskov-Substitutionsprinzip (LSP) : LSP besagt, dass Objekte einer abgeleiteten Klasse in der Lage sein sollten, Objekte der Basisklasse zu ersetzen, ohne die Korrektheit des Programms zu beeinträchtigen. Dieses Prinzip fördert die ordnungsgemäße Verwendung von Vererbung und Polymorphismus.


  • Prinzip der Schnittstellentrennung (ISP) : ISP betont, dass Kunden nicht gezwungen werden sollten, sich auf Schnittstellen zu verlassen, die sie nicht nutzen. Durch die Erstellung kleinerer, fokussierterer Schnittstellen können Entwickler eine bessere Codeorganisation und Wartbarkeit gewährleisten.


  • Prinzip der Abhängigkeitsinversion (DIP) : DIP ermutigt Entwickler, sich auf Abstraktionen statt auf konkrete Implementierungen zu verlassen. Dieses Prinzip fördert eine modularere, testbarere und flexiblere Codebasis.


Wenn Sie sich intensiver mit diesem Thema befassen möchten, wozu ich Sie wärmstens ermutige, dann ist das kein Problem. Im Moment reicht das, was ich präsentiert habe, jedoch aus, um weiterzumachen.


Und was gibt uns SOLID in Bezug auf diesen Artikel?

Saubere Architektur

Robert C. Martin schlug basierend auf den SOLID-Prinzipien und seiner umfangreichen Erfahrung in der Entwicklung verschiedener Anwendungen das Konzept der Clean Architecture vor. Bei der Erörterung dieses Konzepts wird häufig auf das folgende Diagramm verwiesen, um seine Struktur visuell darzustellen:



Clean Architecture ist also kein neues Konzept; Es wird häufig in verschiedenen Programmierparadigmen verwendet, einschließlich funktionaler Programmierung und Backend-Entwicklung.


Bibliotheken wie Lodash und zahlreiche Backend-Frameworks haben diesen Architekturansatz übernommen, der auf den SOLID-Prinzipien basiert.


Clean Architecture legt Wert auf die Trennung von Belangen und die Schaffung unabhängiger, testbarer Schichten innerhalb einer Anwendung, mit dem vorrangigen Ziel, das System leicht verständlich, wartungs- und modifizierbar zu machen.


Die Architektur ist in konzentrischen Kreisen oder Schichten organisiert; Jeder hat klare Grenzen, Abhängigkeiten und Verantwortlichkeiten:


  • Entitäten : Dies sind die zentralen Geschäftsobjekte und Regeln innerhalb der Anwendung. Entitäten sind in der Regel einfache Objekte, die die wesentlichen Konzepte oder Datenstrukturen in der Domäne darstellen, beispielsweise Benutzer, Produkte oder Bestellungen.


  • Anwendungsfälle : Anwendungsfälle, auch Interakteure genannt, definieren die anwendungsspezifischen Geschäftsregeln und orchestrieren die Interaktion zwischen den Entitäten und den externen Systemen. Anwendungsfälle sind für die Implementierung der Kernfunktionalität der Anwendung verantwortlich und sollten unabhängig von den äußeren Schichten sein.


  • Schnittstellenadapter : Diese Komponenten fungieren als Brücke zwischen der inneren und äußeren Schicht und konvertieren Daten zwischen dem Anwendungsfall und externen Systemformaten. Zu den Schnittstellenadaptern gehören Repositorys, Presenter und Controller, die es der Anwendung ermöglichen, mit Datenbanken, externen APIs und UI-Frameworks zu interagieren.


  • Frameworks und Treiber : Diese äußerste Schicht umfasst die externen Systeme wie Datenbanken, UI-Frameworks und Bibliotheken von Drittanbietern. Frameworks und Treiber sind für die Bereitstellung der Infrastruktur verantwortlich, die zum Ausführen der Anwendung und zum Implementieren der in den inneren Schichten definierten Schnittstellen erforderlich ist.


Clean Architecture fördert den Fluss von Abhängigkeiten von den äußeren zu den inneren Schichten und stellt so sicher, dass die Kerngeschäftslogik unabhängig von den verwendeten spezifischen Technologien oder Frameworks bleibt.


Dies führt zu einer flexiblen, wartbaren und testbaren Codebasis, die sich problemlos an sich ändernde Anforderungen oder Technologie-Stacks anpassen lässt.

Atomares Design

Atomic Design ist eine Methodik, die UI-Komponenten organisiert, indem sie Schnittstellen in ihre grundlegendsten Elemente zerlegt und sie dann wieder zu komplexeren Strukturen zusammensetzt. Brad Frost stellte das Konzept erstmals 2008 in einem Artikel mit dem Titel „Atomic Design Methodology“ vor.


Hier ist eine Grafik, die das Konzept von Atomic Design zeigt:



Es besteht aus fünf verschiedenen Ebenen:


  • Atome : Die kleinsten, unteilbaren Einheiten der Schnittstelle, wie z. B. Schaltflächen, Eingaben und Beschriftungen.


  • Moleküle : Gruppen von Atomen, die zusammenarbeiten und komplexere UI-Komponenten wie Formulare oder Navigationsleisten bilden.


  • Organismen : Kombinationen aus Molekülen und Atomen, die unterschiedliche Abschnitte der Benutzeroberfläche erstellen, z. B. Kopf- oder Fußzeilen.


  • Vorlagen : Stellen das Layout und die Struktur einer Seite dar und bieten ein Gerüst für die Platzierung von Organismen, Molekülen und Atomen.


  • Seiten : Instanzen von Vorlagen, die mit tatsächlichen Inhalten gefüllt sind und die endgültige Benutzeroberfläche präsentieren.


Durch die Einführung von Atomic Design können Entwickler von mehreren Vorteilen profitieren, wie z. B. Modularität, Wiederverwendbarkeit und einer klaren Struktur für UI-Komponenten, da wir dazu den Design-System-Ansatz befolgen müssen. Dies ist jedoch nicht das Thema dieses Artikels. Machen Sie also weiter.

Fallstudie: NotionLingo

Um eine fundierte Perspektive auf Clean Architecture für die Frontend-Entwicklung zu entwickeln, begab ich mich auf die Reise, eine Anwendung zu erstellen. Über einen Zeitraum von sechs Monaten habe ich bei der Arbeit an diesem Projekt wertvolle Erkenntnisse und Erfahrungen gesammelt.


Daher basieren die in diesem Artikel bereitgestellten Beispiele auf meiner praktischen Erfahrung mit der Anwendung. Um die Transparenz zu wahren, sind alle Beispiele aus öffentlich zugänglichem Code abgeleitet.


Sie können das Endergebnis erkunden, indem Sie das Repository unter besuchen https://github.com/Levofron/NotionLingo .

Saubere Architekturimplementierung

Wie bereits erwähnt, sind zahlreiche Implementierungen von Clean Architecture online verfügbar. Bei diesen Implementierungen lassen sich jedoch einige gemeinsame Elemente identifizieren:


  • Domänenschicht : Der Kern unserer Anwendung, der geschäftsbezogene Modelle, Anwendungsfälle und Abläufe umfasst.


  • API-Schicht : Verantwortlich für die Interaktion mit Browser-APIs.


  • Repository-Ebene : Dient als Brücke zwischen der Domänen- und der API-Ebene und bietet Platz für die Zuordnung von API-Typen zu unseren Domänentypen.


  • UI-Ebene : Nimmt unsere Komponenten auf und bildet die Benutzeroberfläche.


Wenn wir diese Gemeinsamkeiten verstehen, können wir die grundlegende Struktur von Clean Architecture verstehen und sie an unsere spezifischen Bedürfnisse anpassen.

Domain

Der Kernteil unserer Bewerbung enthält:


  • Anwendungsfälle : Anwendungsfälle beschreiben die Geschäftsregeln für verschiedene Vorgänge, z. B. das Speichern, Aktualisieren und Abrufen von Daten. Ein Anwendungsfall könnte beispielsweise darin bestehen, eine Liste mit Wörtern aus Notion abzurufen oder die tägliche Suche des Benutzers nach gelernten Wörtern zu erhöhen.


    Im Wesentlichen behandeln Use Cases die Aufgaben und Prozesse der Anwendung aus betriebswirtschaftlicher Sicht und stellen so sicher, dass das System gemäß den gewünschten Zielen funktioniert.


  • Modelle : Modelle repräsentieren die Geschäftseinheiten innerhalb der Anwendung. Diese können mithilfe von TypeScript-Schnittstellen definiert werden, um sicherzustellen, dass sie den Bedürfnissen und Geschäftsanforderungen entsprechen.


    Wenn ein Anwendungsfall beispielsweise das Abrufen einer Wortliste aus Notion umfasst, benötigen Sie ein Modell, um die Datenstruktur für diese Liste genau zu beschreiben und dabei die entsprechenden Geschäftsregeln und Einschränkungen einzuhalten.


  • Vorgänge : Manchmal ist es möglicherweise nicht möglich, bestimmte Aufgaben als Anwendungsfälle zu definieren, oder Sie möchten möglicherweise wiederverwendbare Funktionen erstellen, die in mehreren Teilen Ihrer Domäne eingesetzt werden können. Wenn Sie beispielsweise eine Funktion schreiben müssen, um anhand des Namens nach einem Notion-Wort zu suchen, sollten sich diese Operationen hier befinden.


    Operationen sind nützlich, um domänenspezifische Logik zu kapseln, die in verschiedenen Kontexten innerhalb der Anwendung gemeinsam genutzt und genutzt werden kann.


  • Repository-Schnittstellen : Anwendungsfälle erfordern eine Möglichkeit, auf Daten zuzugreifen. Gemäß dem Prinzip der Abhängigkeitsinversion sollte die Domänenschicht nicht von einer anderen Schicht abhängen (während die anderen Schichten von ihr abhängen); Daher definiert diese Schicht die Schnittstellen für die Repositorys.


    Es ist wichtig zu beachten, dass es die Schnittstellen und nicht die Implementierungsdetails angibt. Die Repositorys selbst nutzen das Repository-Muster, das unabhängig von der tatsächlichen Datenquelle ist und die Logik zum Abrufen oder Senden von Daten an und von diesen Quellen hervorhebt.


    Es ist wichtig zu erwähnen, dass ein einzelnes Repository mehrere APIs implementieren kann und ein einzelner Anwendungsfall mehrere Repositorys nutzen kann.

API

Diese Schicht ist für den Datenzugriff verantwortlich und kann bei Bedarf mit verschiedenen Quellen kommunizieren. Da wir eine Frontend-Anwendung entwickeln, dient diese Ebene in erster Linie als Wrapper für Browser-APIs.


Dazu gehören APIs für REST, lokaler Speicher, IndexedDB, Sprachsynthese und mehr.


Es ist wichtig zu beachten, dass die API-Ebene der ideale Ort ist, wenn Sie OpenAPI-Typen und HTTP-Clients generieren möchten. Innerhalb dieser Schicht haben wir:


  • API-Adapter : Der API-Adapter ist ein spezieller Adapter für Browser-APIs, die in unserer Anwendung verwendet werden. Diese Komponente verwaltet REST-Aufrufe und die Kommunikation mit dem Speicher der App oder jeder anderen Datenquelle, die Sie verwenden möchten.


    Bei Bedarf können Sie sogar Ihr eigenes Objektspeichersystem erstellen und implementieren. Durch einen dedizierten API-Adapter können Sie eine konsistente Schnittstelle für die Interaktion mit verschiedenen Datenquellen beibehalten und so die Aktualisierung oder Änderung bei Bedarf erleichtern.


  • Typen : Dies ist ein Ort für alle Typen, die sich auf Ihre API beziehen. Diese Typen beziehen sich nicht direkt auf die Domäne, sondern dienen als Beschreibungen der von der API empfangenen Rohantworten. In der nächsten Ebene werden diese Typen für die ordnungsgemäße Zuordnung und Verarbeitung von entscheidender Bedeutung sein.

Repository

Die Repository-Schicht spielt eine entscheidende Rolle in der Architektur der Anwendung, indem sie die Integration mehrerer APIs verwaltet, API-spezifische Typen Domänentypen zuordnet und Vorgänge zur Datentransformation integriert.


Wenn Sie beispielsweise die Sprachsynthese-API mit lokaler Speicherung kombinieren möchten, sind Sie hier genau richtig. Diese Ebene enthält:


  • Repository-Implementierung : Dies sind konkrete Implementierungen der in der Domänenschicht deklarierten Schnittstellen. Sie sind in der Lage, mit mehreren Datenquellen zu arbeiten und gewährleisten so Flexibilität und Anpassungsfähigkeit innerhalb der Anwendung.


  • Operationen : Diese können als Mapper, Transformer oder Helfer bezeichnet werden. In diesem Zusammenhang ist „Operationen“ ein passender Begriff. Dieses Verzeichnis enthält alle Funktionen, die für die Zuordnung roher API-Antworten zu den entsprechenden Domänentypen verantwortlich sind, um sicherzustellen, dass die Daten für die Verwendung innerhalb der Anwendung ordnungsgemäß strukturiert sind.

Adapter


Die Adapterschicht ist dafür verantwortlich, die Interaktionen zwischen diesen Schichten zu orchestrieren und sie miteinander zu verbinden. Diese Schicht enthält nur Module, die verantwortlich sind für:


  • Abhängigkeitsinjektion : Die Adapterschicht verwaltet die Abhängigkeiten zwischen den API-, Repository- und Domänenschichten. Durch die Handhabung der Abhängigkeitsinjektion sorgt die Adapterschicht für eine saubere Trennung der Anliegen und fördert eine effiziente Wiederverwendbarkeit des Codes.


  • Modulorganisation : Die Adapterschicht organisiert die Anwendung in Module basierend auf ihren Funktionalitäten (z. B. lokaler Speicher, REST, Sprachsynthese, Supabase). Jedes Modul kapselt eine bestimmte Funktionalität und bietet so eine saubere und modulare Struktur für die Anwendung.


  • Erstellen von Aktionen : Die Adapterschicht erstellt Aktionen, indem sie Anwendungsfälle aus der Domänenschicht mit den entsprechenden Repositorys kombiniert. Diese Aktionen dienen als Einstiegspunkte für die Anwendung, um mit den darunter liegenden Schichten zu interagieren.

Präsentation

Die Präsentationsschicht ist für die Darstellung der Benutzeroberfläche (UI) und die Abwicklung von Benutzerinteraktionen mit der Anwendung verantwortlich. Es nutzt die Adapter-, Domänen- und freigegebenen Ebenen, um eine funktionale und interaktive Benutzeroberfläche zu erstellen.


Die Präsentationsschicht nutzt die Atomic-Design-Methodik zur Organisation ihrer Komponenten, was zu einer skalierbaren und wartbaren Anwendung führt. Diese Ebene wird jedoch nicht der Hauptschwerpunkt dieses Artikels sein, da sie nicht das Hauptthema im Hinblick auf die Implementierung von Clean Architecture ist.

Geteilt

Für alle gemeinsamen Elemente, wie zentralisierte Dienstprogramme, Konfigurationen und gemeinsame Logik, ist ein bestimmter Ort erforderlich. Wir werden in diesem Artikel jedoch nicht zu tief auf diese Ebene eingehen.


Es lohnt sich, dies nur zu erwähnen, um ein Verständnis dafür zu vermitteln, wie gemeinsame Komponenten in der gesamten Anwendung verwaltet und gemeinsam genutzt werden.

Teststrategien für jede Schicht

Bevor wir uns nun mit dem Codieren befassen, ist es wichtig, das Testen zu besprechen. Es ist von entscheidender Bedeutung, die Zuverlässigkeit und Korrektheit Ihrer Anwendung sicherzustellen und eine robuste Teststrategie für jede Ebene der Architektur zu implementieren.


  • Domänenschicht : Unit-Tests sind die primäre Methode zum Testen der Domänenschicht. Konzentrieren Sie sich auf das Testen von Domänenmodellen, Validierungsregeln und Geschäftslogik und stellen Sie sicher, dass sie sich unter verschiedenen Bedingungen korrekt verhalten. Führen Sie testgetriebene Entwicklung (TDD) ein, um das Design Ihrer Domänenmodelle voranzutreiben und zu bestätigen, dass Ihre Geschäftslogik solide ist.


  • API-Schicht : Testen Sie die API-Schicht mithilfe von Integrationstests. Diese Tests sollten sich darauf konzentrieren, sicherzustellen, dass die API korrekt mit externen Diensten interagiert und dass die Antworten richtig formatiert sind. Nutzen Sie Tools wie automatisierte Test-Frameworks wie Jest, um API-Aufrufe zu simulieren und die Antworten zu validieren.


  • Repository-Schicht : Für die Repository-Schicht können Sie eine Kombination aus Unit- und Integrationstests verwenden. Unit-Tests können verwendet werden, um einzelne Repository-Methoden zu testen, während sich Integrationstests darauf konzentrieren sollten, zu überprüfen, ob die Repositorys korrekt mit ihren APIs interagieren.


  • Adapterschicht : Unit-Tests eignen sich zum Testen der Adapterschicht. Diese Tests sollen sicherstellen, dass die Adapter Abhängigkeiten korrekt einfügen und Datentransformationen zwischen Ebenen verwalten. Das Verspotten der Abhängigkeiten, etwa der API- oder Repository-Schichten, kann dabei helfen, die Adapterschicht während des Tests zu isolieren.


Durch die Implementierung einer umfassenden Teststrategie für jede Ebene der Architektur können Sie die Zuverlässigkeit, Korrektheit und Wartbarkeit Ihrer Anwendung sicherstellen und gleichzeitig die Wahrscheinlichkeit von Fehlern während der Entwicklung verringern.


Wenn Sie jedoch eine kleine Anwendung erstellen, sollten Integrationstests auf der Adapterebene ausreichen.

Lasst uns etwas programmieren

Okay, jetzt, da Sie ein solides Verständnis von Clean Architecture haben und sich vielleicht sogar eine eigene Meinung dazu gebildet haben, lassen Sie uns etwas tiefer eintauchen und echten Code erkunden.


Bedenken Sie, dass ich hier nur ein einfaches Beispiel präsentiere; Wenn Sie jedoch an detaillierteren Beispielen interessiert sind, können Sie gerne mein GitHub-Repository erkunden, das am Anfang dieses Artikels erwähnt wurde.


Im „echten Leben“ glänzt Clean Architecture in großen Anwendungen auf Unternehmensebene wirklich, während es für kleinere Projekte möglicherweise übertrieben ist. Nachdem dies gesagt ist, kommen wir zum Punkt.


Am Beispiel meiner Anwendung zeige ich, wie man einen API-Aufruf durchführt, um Wörterbuchvorschläge für ein bestimmtes Wort abzurufen. Dieser spezielle API-Endpunkt ruft durch Web-Scraping zweier Websites eine Liste mit Bedeutungen und Beispielen ab.


Aus geschäftlicher Sicht ist dieser Endpunkt von entscheidender Bedeutung für die Ansicht „Wort suchen“, die es Benutzern ermöglicht, nach einem bestimmten Wort zu suchen. Sobald der Benutzer das Wort gefunden und sich angemeldet hat, kann er die im Internet gespeicherten Informationen zu seiner Notion-Datenbank hinzufügen.

Ordnerstruktur

Zunächst müssen wir eine Ordnerstruktur einrichten, die die zuvor besprochenen Ebenen genau widerspiegelt. Der Aufbau sollte wie folgt aussehen:


 client ├── adapter ├── api ├── domain ├── presentation ├── repository └── shared


Das Client-Verzeichnis dient in vielen Projekten einem ähnlichen Zweck wie der Ordner „src“. In diesem speziellen Next.js-Projekt habe ich die Konvention übernommen, den Frontend-Ordner als „Client“ und den Backend-Ordner als „Server“ zu benennen.


Dieser Ansatz ermöglicht eine klare Unterscheidung zwischen den beiden Hauptkomponenten der Anwendung.

Unterverzeichnisse

Die Wahl der richtigen Ordnerstruktur für Ihr Projekt ist in der Tat eine entscheidende Entscheidung, die frühzeitig im Entwicklungsprozess getroffen werden sollte. Verschiedene Entwickler haben ihre eigenen Vorlieben und Ansätze, wenn es um die Organisation von Ressourcen geht.


Einige gruppieren Ressourcen möglicherweise nach Seitennamen, andere folgen möglicherweise den von OpenAPI generierten Namenskonventionen für Unterverzeichnisse und wieder andere glauben möglicherweise, dass ihre Anwendung zu klein ist, um eine dieser Lösungen zu rechtfertigen.


Der Schlüssel liegt darin, eine Struktur zu wählen, die den spezifischen Anforderungen und dem Umfang Ihres Projekts am besten entspricht und gleichzeitig eine klare und wartbare Ressourcenorganisation aufrechterhält.


Ich gehöre zur dritten Gruppe, daher sieht meine Struktur so aus:


 client ├── adapter │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── api │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── domain │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ ├── supabase └── repository ├── local-storage ├── rest ├── speech-synthesis └── supabase


Ich habe beschlossen, die geteilten und Präsentationsebenen in diesem Artikel wegzulassen, da ich glaube, dass diejenigen, die tiefer eintauchen möchten, für weitere Informationen auf mein Repository verweisen können. Fahren wir nun mit einigen Codebeispielen fort, um zu veranschaulichen, wie Clean Architecture in einer Frontend-Anwendung angewendet werden kann.

Domänendefinition

Betrachten wir unsere Anforderungen. Als Nutzer möchte ich eine Liste mit Vorschlägen inklusive Bedeutung und Beispielen erhalten. Daher kann ein einzelner Wörterbuchvorschlag wie folgt modelliert werden:


 interface DictionarySuggestion { example: string; meaning: string; }


Nachdem wir nun einen einzelnen Wörterbuchvorschlag beschrieben haben, ist es wichtig zu erwähnen, dass das durch Web Scraping erhaltene Wort manchmal von der Eingabe des Benutzers abweicht oder korrigiert wird. Um dies zu berücksichtigen, verwenden wir später in unserer App die korrigierte Version.


Daher müssen wir eine Schnittstelle definieren, die eine Liste mit Wörterbuchvorschlägen und Wortkorrekturen enthält. Die endgültige Schnittstelle sieht folgendermaßen aus:


 export interface DictionarySuggestions { suggestions: DictionarySuggestion[]; word: string; }


Wir exportieren diese Schnittstelle, weshalb das Schlüsselwort export enthalten ist.

Repository-Schnittstelle

Wir haben unser Modell und jetzt ist es an der Zeit, es in die Tat umzusetzen.


 import { DictionarySuggestions } from './rest.models'; export interface RestRepository { getDictionarySuggestions: (word: string) => Promise<DictionarySuggestions | null>; }


An diesem Punkt sollte alles klar sein. Es ist wichtig zu beachten, dass wir hier überhaupt nicht auf die API eingehen! Die Struktur des Repositorys selbst ist recht einfach: nur ein Objekt mit einigen Methoden, wobei jede Methode asynchron Daten eines bestimmten Typs zurückgibt.


Bitte beachten Sie, dass das Repository Daten immer im Domänenmodellformat zurückgibt.

Anwendungsfall

Definieren wir nun unsere Geschäftsregel als Anwendungsfall. Der Code sieht so aus:


 export type GetDictionarySuggestionsUseCaseUseCase = UseCaseWithSingleParamAndPromiseResult< string, DictionarySuggestions | null >; export const getDictionarySuggestionsUseCase = ( restRepository: RestRepository, ): GetDictionarySuggestionsUseCaseUseCase => ({ execute: (word) => restRepository.getDictionarySuggestions(word), });


Als Erstes ist die Liste der gängigen Typen zu beachten, die zum Definieren von Anwendungsfällen verwendet werden. Um dies zu erreichen, habe ich eine Datei use-cases.types.ts im Domänenverzeichnis erstellt:


 domain ├── local-storage ├── rest ├── speech-synthesis ├── supabase └── use-cases.types.ts


Dadurch kann ich Typen für Anwendungsfälle problemlos zwischen meinen Unterverzeichnissen teilen. Die Definition von UseCaseWithSingleParamAndPromiseResult sieht folgendermaßen aus:


 export interface UseCaseWithSingleParamAndPromiseResult<TParam, TResult> { execute: (param: TParam) => Promise<TResult>; }


Dieser Ansatz trägt dazu bei, die Konsistenz und Wiederverwendbarkeit von Anwendungsfalltypen auf der gesamten Domänenebene aufrechtzuerhalten.


Sie fragen sich vielleicht, warum wir die execute benötigen. Hier haben wir eine Fabrik, die den tatsächlichen Anwendungsfall zurückgibt.


Diese Designwahl ist auf die Tatsache zurückzuführen, dass wir die Repository-Implementierung nicht direkt im Anwendungsfallcode referenzieren möchten und auch nicht möchten, dass das Repo von einem Import verwendet wird. Dieser Ansatz ermöglicht es uns, die Abhängigkeitsinjektion später einfach anzuwenden.


Durch die Verwendung des Factory-Musters und der execute können wir die Implementierungsdetails des Repositorys vom Anwendungsfallcode trennen, was die Modularität und Wartbarkeit der Anwendung verbessert.


Dieser Ansatz folgt dem Dependency Inversion-Prinzip, bei dem die Domänenschicht nicht von einer anderen Schicht abhängig ist und eine größere Flexibilität ermöglicht, wenn es darum geht, verschiedene Repository-Implementierungen auszutauschen oder die Architektur der Anwendung zu ändern.

API-Definition

Definieren wir zunächst unsere Schnittstelle:


 export interface RestApi { getDictionarySuggestions: (word: string) => Promise<AxiosResponse<DictionarySuggestions>>; }


Wie Sie sehen, ähnelt die Definition dieser Funktion in der Schnittstelle stark der im Repository. Da der Domänentyp die Antwort bereits beschreibt, ist es nicht erforderlich, denselben Typ neu zu erstellen.


Es ist wichtig zu beachten, dass unsere API Rohdaten zurückgibt, weshalb wir den vollständigen AxiosResponse<DictionarySuggestions> zurückgeben. Dadurch wahren wir eine klare Trennung zwischen der API- und der Domänenebene und ermöglichen so mehr Flexibilität bei der Datenverarbeitung und -transformation.


Die Implementierung dieser API sieht folgendermaßen aus:


 export const getRestApi = (axiosInstance: AxiosInstance): RestApi => ({ getDictionarySuggestions: async (word: string) => { const encodedCurrentDate = encodeURIComponent(word); const response = await axiosInstance.get( `${RestEndpoints.GET_DICTIONARY_SUGGESTIONS}?word=${encodedCurrentDate}`, ); return response; } });


An diesem Punkt wird es interessanter. Der erste wichtige Aspekt, den es zu besprechen gilt, ist die Injektion unserer axiosInstance . Dies macht unseren Code sehr flexibel und ermöglicht uns die einfache Erstellung solider Tests. Hier kümmern wir uns auch um die Codierung oder das Parsen von Abfrageparametern.


Sie können hier jedoch auch andere Aktionen ausführen, beispielsweise das Trimmen der Eingabezeichenfolge. Durch das Einfügen von axiosInstance sorgen wir für eine klare Trennung der Belange und stellen sicher, dass die API-Implementierung an verschiedene Szenarien oder Änderungen in den externen Diensten anpassbar ist.

Repository-Implementierung

Da unsere Schnittstelle bereits durch die Domäne definiert ist, müssen wir nur noch unser Repository implementieren. Die endgültige Implementierung sieht also so aus:

 export const getRestRepository = (restApi: RestApi): RestRepository => ({ getDictionarySuggestions: async (word) => { const { data } = await restApi.getDictionarySuggestions(word); if (!data?.suggestions?.length) { return null; } return formatDictionarySuggestions(data); } });


Ein wichtiger zu erwähnender Aspekt betrifft APIs. Unser getRestRepository ermöglicht es uns, eine zuvor definierte restApi zu übergeben. Dies ist von Vorteil, da es, wie bereits erwähnt, ein einfacheres Testen ermöglicht. Wir können formatDictionarySuggestions kurz untersuchen:


 export const formatDictionarySuggestions = ({ suggestions, word, }: DictionarySuggestions): DictionarySuggestions => { const cleanedWord = cleanUpString(word); const cleanedSuggestions = suggestions.map((_suggestion) => { const cleanedMeaning = cleanUpString(_suggestion.meaning); const cleanedExample = cleanUpString(_suggestion.example); return { meaning: cleanedMeaning, example: cleanedExample, }; }); return { word: cleanedWord, suggestions: cleanedSuggestions, }; };


Dieser Vorgang verwendet unser Domänen- DictionarySuggestions Modell als Argument und führt eine String-Bereinigung durch, was bedeutet, dass unnötige Leerzeichen, Zeilenumbrüche, Tabulatoren und Groß-/Kleinschreibung entfernt werden. Es ist ziemlich einfach, ohne versteckte Komplexität.


Es ist wichtig zu beachten, dass Sie sich an dieser Stelle keine Gedanken über Ihre API-Implementierung machen müssen. Zur Erinnerung: Das Repository gibt Daten immer im Domänenmodell zurück! Es kann nicht anders sein, da dies das Prinzip der Abhängigkeitsumkehr verletzen würde.


Und im Moment ist unsere Domänenschicht nicht von irgendetwas außerhalb ihr abhängig.

Adapter – Lassen Sie uns das alles zusammenfügen

Zu diesem Zeitpunkt sollte alles implementiert und für die Abhängigkeitsinjektion bereit sein. Hier ist die endgültige Implementierung des restlichen Moduls:


 import { getRestRepository } from '@repository/rest/rest.repository'; import { getRestApi } from '@api/rest/rest.api'; import { getDictionarySuggestionsUseCase } from '@domain/rest/rest.use-cases'; import { axiosInstance } from '@shared/axios.instance'; const restApi = getRestApi(axiosInstance); const restRepository = getRestRepository(restApi); export const restModule = { getDictionarySuggestions: getDictionarySuggestionsUseCase(restRepository).execute, };


Das ist richtig! Wir haben den Prozess der Implementierung von Clean Architecture-Prinzipien durchlaufen, ohne an ein bestimmtes Framework gebunden zu sein. Dieser Ansatz stellt sicher, dass unser Code anpassbar ist, sodass bei Bedarf problemlos zwischen Frameworks oder Bibliotheken gewechselt werden kann.


Wenn es um Tests geht, ist das Auschecken des Repositorys eine gute Möglichkeit, zu verstehen, wie Tests in dieser Architektur implementiert und organisiert werden.


Mit einer soliden Grundlage in Clean Architecture können Sie umfassende Tests schreiben, die verschiedene Szenarien abdecken und so Ihre Anwendung robuster und zuverlässiger machen.


Wie gezeigt, führt die Befolgung der Clean Architecture-Prinzipien und die Trennung von Bedenken zu einer wartbaren, skalierbaren und testbaren Anwendungsstruktur.


Dieser Ansatz erleichtert letztendlich das Hinzufügen neuer Funktionen, die Umgestaltung von Code und die Zusammenarbeit mit einem Team an einem Projekt und stellt so den langfristigen Erfolg Ihrer Anwendung sicher.

Präsentation

In der Beispielanwendung wird React für die Präsentationsschicht verwendet. Im Adapterverzeichnis gibt es eine zusätzliche Datei namens hooks.ts , die die Interaktion mit dem restlichen Modul abwickelt. Der Inhalt dieser Datei ist wie folgt:


 import { restModule } from '@adapter/rest/rest.module'; import { useAxios } from '@shared/hooks'; export const useDictionarySuggestions = () => { const { data, error, isLoading, mutate } = useAxios(restModule.getDictionarySuggestions); return { dictionarySuggestions: data, getDictionarySuggestions: mutate, dictionarySuggestionsError: error, isDictionarySuggestionsLoading: isLoading, }; };


Diese Implementierung macht die Arbeit mit der Präsentationsschicht unglaublich einfach. Durch die Verwendung des useDictionarySuggestions Hooks muss sich die Präsentationsschicht nicht um die Verwaltung von Datenzuordnungen oder anderen Verantwortlichkeiten kümmern, die nichts mit ihrer primären Funktion zu tun haben.


Diese Trennung der Belange trägt dazu bei, die Prinzipien der Clean Architecture beizubehalten und führt zu besser verwaltbarem und wartbarem Code.

Was kommt als nächstes?

Zuallererst empfehle ich Ihnen, in den Code des bereitgestellten GitHub-Repos einzutauchen und seine Struktur zu erkunden.


Was kannst du noch tun? Der Himmel ist das Limit! Es hängt alles von Ihren spezifischen Designanforderungen ab. Beispielsweise könnten Sie erwägen, die Datenschicht durch die Einbindung eines Datenspeichers zu implementieren (Redux, MobX oder sogar etwas Benutzerdefiniertes – das spielt keine Rolle).


Alternativ könnten Sie mit verschiedenen Kommunikationsmethoden zwischen den Schichten experimentieren, wie zum Beispiel die Verwendung von RxJS zur Abwicklung der asynchronen Kommunikation mit dem Backend, was Abfragen, Push-Benachrichtigungen oder Sockets umfassen könnte (im Wesentlichen die Vorbereitung auf jede Datenquelle).


Grundsätzlich können Sie nach Belieben erkunden und experimentieren, solange Sie die geschichtete Architektur beibehalten und sich an das Prinzip der umgekehrten Abhängigkeit halten. Stellen Sie immer sicher, dass die Domäne im Mittelpunkt Ihres Designs steht.


Auf diese Weise erstellen Sie eine flexible und wartbare Anwendungsstruktur, die sich an verschiedene Szenarien und Anforderungen anpassen lässt.

Zusammenfassung

In diesem Artikel haben wir uns mit dem Konzept der Clean Architecture im Kontext einer mit React erstellten Sprachlernanwendung befasst.


Wir haben die Bedeutung der Aufrechterhaltung einer mehrschichtigen Architektur und die Einhaltung des Prinzips der umgekehrten Abhängigkeit sowie die Vorteile der Trennung von Belangen hervorgehoben.


Ein wesentlicher Vorteil von Clean Architecture besteht darin, dass Sie sich auf den technischen Aspekt Ihrer Anwendung konzentrieren können, ohne an ein bestimmtes Framework gebunden zu sein. Diese Flexibilität ermöglicht es Ihnen, Ihre Anwendung an verschiedene Szenarien und Anforderungen anzupassen.


Dieser Ansatz weist jedoch einige Nachteile auf. In manchen Fällen kann die Einhaltung eines strengen Architekturmusters zu mehr Boilerplate-Code oder zusätzlicher Komplexität in der Projektstruktur führen.


Darüber hinaus kann es sowohl ein Vor- als auch ein Nachteil sein, sich weniger auf die Dokumentation zu verlassen – es ermöglicht zwar mehr Freiheit und Kreativität, kann aber auch zu Verwirrung oder Missverständnissen zwischen den Teammitgliedern führen.


Trotz dieser potenziellen Herausforderungen kann die Implementierung von Clean Architecture äußerst vorteilhaft sein, insbesondere im Kontext von React, wo es kein allgemein akzeptiertes Architekturmuster gibt.


Es ist wichtig, Ihre Architektur zu Beginn eines Projekts zu berücksichtigen, anstatt sie erst nach Jahren des Ringens in Angriff zu nehmen.


Um ein reales Beispiel von Clean Architecture in Aktion zu sehen, schauen Sie sich gerne mein Repository unter an https://github.com/Levofron/NotionLingo . Über die in meinem Profil bereitgestellten Links können Sie auch über soziale Medien mit mir in Kontakt treten.


Wow, das ist wahrscheinlich der längste Artikel, den ich je geschrieben habe. Es fühlt sich unglaublich an!