Frontend-Komponenten nach dem Prinzip der Verantwortung statt der Wiederverwendung entwerfen
Erfahren Sie, warum die Organisation von Frontend-Komponenten nach klaren Eigentumsverhältnissen und Verantwortlichkeiten – anstelle von maximaler Code-Wiederverwendung – es erleichtert, Änderungen an Funktionen besser zu isolieren und zu warten.
Ein Frontend wird leichter zu warten, wenn Komponentengrenzen anhand klar definerter Verantwortungsbereiche festgelegt werden statt danach, wie viel Code theoretisch geteilt werden kann.
Das Problem mit unserem Frontend lag nie darin, dass einzelne Komponenten zu groß geworden wären. Die meisten von ihnen waren tatsächlich kompakt.
Wir verfügten über eine solide Bibliothek mit wiederverwendbaren Buttons, Karten, Modalen, Tabellenelementen, Formulareingabefeldern, Hooks, API-Hilfsfunktionen, gemeinsamen Schemata sowie einer ordentlichen Ordnerstruktur. Im Repository wirkte alles gut organisiert. Die Wiederverwendung war weit verbreitet, offensichtliche Duplikate selten, und das Abstraktionsniveau verlieh der Codebasis ein Gefühl von Reife.
Das eigentliche Problem trat immer dann auf, wenn eine Funktion geändert werden musste.
Sogar eine geringfügige Anforderung konnte dazu führen, dass wir gleichzeitig in mehreren unzusammenhängenden Schichten suchen mussten: ein komponentenbasiertes Element auf Bildschirmebene, ein allgemein einsetzbares Formular, einige gemeinsam genutzte Logiken in Form von Hooks, Code für das Netzwerk, ein Dialogfeld für viele Anwendungsfälle sowie ein Schema zur Überprüfung der Eingaben. Eine Komponente, die ursprünglich gemeinsam genutzt wurde, weil zwei Bildschirme ähnlich aussahen, sammelte allmählich Flaggen, Callback-Propertien, bedingte Validierungsregeln, alternative Layouts sowie Sonderfälle für Workflows an, von denen sie ursprünglich nichts wissen sollte.
Der Code war wiederverwendbar, doch die Verantwortlichkeiten waren im gesamten System verteilt.
Was letztendlich die Frontend-Struktur vereinfachte, war weder ein neues Verzeichnisnamensschema, eine andere State-Management-Bibliothek noch eine strenge Zeilenanzahlbegrenzung pro Komponente. Es war vielmehr ein Wandel in unserer Auffassung davon, was die Grenzen einer Komponente eigentlich darstellen sollten.
Anstatt nur zu fragen:
Kann dieses Komponente an anderer Stelle wiederverwendet werden?
Wir begannen, folgende Frage zu stellen:
Für welche Verantwortung sollte diese Komponente zuständig sein?
Schnell wurde eine zweite Frage genauso wichtig:
Wenn sich diese Verantwortung ändern muss, wie viel unverwandter Code wird dabei mitgezogen?
Diese Frage führte dazu, dass unsere Architektur zu einer einfachen Hierarchie umgestaltet wurde, bei der die mittlere Schicht den größten Anteil übernahm.
Wiederverwendbare Primitiven kümmerten sich ausschließlich um die Darstellung. Funktionskomponenten enthielten das Verständnis der Geschäftsworkflows. Seiten sowie andere Kompositionsgrenzen bestimmten, wie alle Bestandteile miteinander verbunden wurden.
Die Arbeit am Frontend wurde einfacher – nicht, weil wir alle Duplikate beseitigt haben, sondern weil Änderungen auf Funktionsebene besser kontrolliert werden konnten und weniger Teile der Codebasis voneinander verstanden werden mussten.
1. Wiederverwendung ist nicht dasselbe wie Einfachheit
„Vermeiden Sie die Duplikation von Komponenten“ ist ein Ratschlag, der im Allgemeinen richtig klingt.
Oft ist er das auch tatsächlich.
Wenn zwei Bildschirme denselben Button, ein Eingabefeld oder eine Modalliste teilen, verringert die Vereinheitlichung dieser Implementierung Inkonsistenzen und zusätzliche Wartungsarbeiten.
Probleme entstehen, wenn eine visuelle Ähnlichkeit fälschlicherweise als gemeinsame Verantwortung interpretiert wird.
Stellen Sie sich zwei separate Funktionen vor, die beide eine Bestätigungsdialogbox benötigen.
Die erste Implementierung könnte so aussehen:
<ConfirmationDialog
title="Delete project?"
onConfirm={deleteProject}
/>
Dann benötigt ein anderer Workflow denselben Dialog, allerdings ohne Abbrech-Button.
In einem dritten Fall ist eine benutzerdefinierte Warnmeldung erforderlich.
In einem weiteren Fall muss vor der Bestätigung durch den Benutzer eine asynchrone Überprüfung durchgeführt werden.
Bald entwickelt sich das Komponentenmodell zu etwas wie diesem:
<ConfirmationDialog
title="Delete project?"
variant="danger"
showCancel
disableConfirm={isDeleting}
customWarning={warning}
onBeforeConfirm={validateDeletion}
onConfirm={deleteProject}
onSpecialAction={archiveInstead}
useLegacyLayout={false}
/>
Technisch gesehen wird die Komponente weiterhin wiederverwendet.
Aber architektonisch gesehen hat sie sich zu einer Art Konfigurationssprache entwickelt.
Jeder neue Workflow verlangt von der gemeinsam genutzten Komponente, eine weitere Variante zu unterstützen. Dadurch wird sie flexibler, doch diese Flexibilität kommt zum Preis von immer mehr verstecktem Verhalten hinter einer stetig wachsenden Anzahl an Eigenschaften.
Genau hier weicht der Preis der Abstraktion vom Preis der Duplikation ab.
Duplikation zeigt sich sofort – zwei nahezu identische Komponenten befinden sich nebeneinander im Repository und sind jedem, der den Code liest, eindeutig sichtbar.
Eine schlecht gewählte Abstraktion wirkt auf den ersten Blick billig. Ihre wahren Kosten zeigen sich erst später, wenn die von ihr bedienten Funktionen in unterschiedliche Richtungen entwickeln.
Duplikation kostet Sie etwas, das Sie sofort erkennen können. Die falsche Abstraktion kostet Sie etwas, das Sie erst dann bemerken, wenn die „ähnlichen“ Funktionen nicht mehr synchron weiterentwickelt werden.
Diese Erkenntnis veränderte unsere Art, Wiederverwendung zu bewerten.
Wiederholter JSX löste nicht mehr automatisch Misstrauen aus. Die wichtigere Frage war, ob zwei Codeabschnitte tatsächlich dieselbe Verantwortung teilten oder sich einfach zu diesem Zeitpunkt zufällig ähnelten.
2. Wir begannen, Komponenten um die Verantwortung herum zu entwerfen
Der bedeutendste architektonische Wandel bestand darin, den Komponentenbaum dem tatsächlichen Verantwortungsbereich anzupassen anstelle darauf zu achten, Wiederverwendungspotenziale zu nutzen.
Nehmen wir einen Konfigurationsworkflow als Beispiel.
Jede Schicht in diesem Workflow existiert aus einem eigenen, spezifischen Grund.
UserSettingsPage integriert die Funktion in den Rest der Anwendung. Sie kann sich mit dem Routing, dem Layout auf Seiteebene oder damit beschäftigen, welches Konto derzeit bearbeitet wird.
UserSettingsForm enthält das Wissen über den Workflow. Sie versteht, welche Daten zu den Benutzereinstellungen gehören, wie sich die Felder zueinander verhalten, was eine Übermittlung bedeutet und wie Fehler dem Benutzer angezeigt werden sollten.
Button, Input und Checkbox haben keine Ahnung davon, dass Benutzereinstellungen überhaupt existieren. Sie bleiben wiederverwendbar, genau weil ihre Aufgaben tatsächlich allgemein einsetzbar sind.
Diese mittlere Funktionsschicht ist es, die Teams in der Regel zuerst verlieren.
In einer Frontend-Architektur, die die Wiederverwendung zu aggressiv verfolgt, springen Entwickler oft direkt von einer Seite zu generischen Komponenten und umgehen dabei jede feature-spezifische Schicht.
Dann hat das Geschäftsverhalten keinen klaren Platz mehr.
Es gelangt stattdessen in die Konfiguration ein.
Die generische Formularstruktur erfährt, dass ein bestimmter Workflow ein Benutzernamenfeld benötigt, während ein anderer keines braucht. Die generische Tabelle weiß, welche Aktionen für eine bestimmte Art von Domänenobjekt gültig sind. Ein gemeinsames Modal-Fenster übernimmt Sonderfälle einfach deshalb, weil ein Workflow zufällig seinen Inhalt innerhalb eines Modals anzeigt.
Die wiederverwendbare Schicht nimmt allmählich Kenntnisse über das Geschäft auf, die sie ursprünglich nicht tragen sollte.
Durch ein Design, das auf Verantwortungsbereichen basiert, wird dieser Druck umgekehrt.
Feature-spezifischen Komponenten wird gestattet, das Feature zu verstehen, dem sie dienen.
Wiederverwendbare Primitiven werden absichtlich davon abgehalten, etwas von fachspezifischen Eigenschaften mitzubekommen.
Diese Trennung macht die gesamte Architektur leichter verständlich, denn jede Schicht muss nur mit einer geringeren Menge an Kontext arbeiten.
3. Zweckbezogene Komponenten haben ihren Platz verdient
Einer der schwierigsten Gewohnheiten, die man ablegen muss, war die Annahme, dass eine Komponente, die nur an einer Stelle verwendet wird, ein Designfehler darstellt.
Nehmen wir folgendes Beispiel:
<OrderCancellationDialog />
Bereits der Name zeigt, dass diese Komponente für einen einzigen Workflow entwickelt wurde.
Vergleichen Sie das mit etwas wie:
<ConfirmationDialog />
Der zweite Name wirkt wiederverwendbarer.
Doch das Stornieren einer Bestellung erfordert letztendlich weitaus mehr als nur eine einfache Bestätigung.
Der Benutzer muss möglicherweise einen Stornegrund auswählen. Auf dem Bildschirm könnten Details zur Rückerstattung angezeigt werden. Bestimmte Bestellungen sind möglicherweise nicht stornierbar. Die Berechtigungsprüfungen können variieren. Die Warnhinweise können je nach Lieferstatus ändern. Die Aktion selbst kann ihre eigene asynchrone Fehlerbehandlung haben.
Wenn all das in ein generisches ConfirmationDialog eingebettet wird, entwickelt sich das gemeinsame Komponentenmodul allmählich zu einem Experten dafür, was das Stornieren einer Bestellung tatsächlich bedeutet.
Eine bessere Struktur sieht in der Regel so aus:
OrderCancellationDialog übernimmt die Verantwortung für den gesamten Workflow.
Es darf über Berechtigungen, Stornegründe, asynchrone Status, Rückerstattungstexte sowie Fehlerzustände Bescheid wissen, denn all das gehört naturgemäß zusammen.
Gleichzeitig bleiben die untergeordneten Bausteine genau deshalb wiederverwendbar, weil sie keinerlei Kenntnisse über Bestellungen besitzen.
Diese Aufteilung veränderte, wie viele Entscheidungen bezüglich der Komponenten getroffen wurden.
Es bestand nicht länger die Notwendigkeit, das Vorhandensein einer Komponente dadurch zu rechtfertigen, wie viele Stellen sie importierten.
Wiederverwendbarkeit ist keine Voraussetzung für jede Komponente. Einige Komponenten existieren ausschließlich dazu, einer bestimmten Geschäftslogik einen geeigneten Platz zu bieten.
Eine Komponente mit nur einer Verwendungsmöglichkeit kann dennoch ihren Beitrag leisten, wenn sie es ermöglicht, einen Workflow später leichter zu finden, zu verstehen und zu ändern.
Diese Art von lokaler Klarheit überwiegt oft den Aufwand, einen zweiten, unverwandten Workflow aus rein äußerlichen API-Ähnlichkeiten heraus in eine gemeinsame Abstraktion zu zwingen.
4. Geschäftslogik blieb außerhalb der Anzeigekomponenten
Das gleiche Verantwortlichkeitsproblem trat erneut bei der Datenverarbeitung und Infrastrukturfragen auf.
Ein ursprünglich rein visueller Komponent kann allmählich Aufgaben wie das Abrufen von Daten, das Lesen von URL-Parametern, das Ausführen von Mutationen, das Auslösen von Benachrichtigungen, die Handhabung der Navigation, das Verwalten des Caches sowie das Erfassen des Workflow-Zustands übernehmen.
Stellen Sie sich eine ProjectTable vor, die letztendlich all das bewältigt:
ProjectTable
├── fetch projects
├── read query parameters
├── filter projects
├── manage loading state
├── render rows
├── delete projects
├── show notifications
└── navigate after actions
Der Name deutet immer noch auf „Tabelle“ hin.
Doch die Komponente versteht inzwischen einen großen Teil der Funktionalität.
Deshalb ist es schwierig, sie an anderer Stelle wiederverzuenden, da die Wiederverwendung der Tabelle auch Annahmen bezüglich des Datenabrufs, der Mutationen, der Navigation, des Cachens sowie von Nebeneffekten mit sich bringt.
Eine nach Verantwortungsbereichen organisierte Struktur könnte eher so aussehen:
Der auf Funktionalitäten ausgerichtete Code bestimmt, wie das „Laden“ aussieht, wie Fehler behandelt werden, was durch Löschvorgänge tatsächlich erreicht wird, welche Filter auf diesen bestimmten Workflow angewendet werden und ob der Benutzer danach umgeleitet werden sollte.
ProjectTable konzentriert sich selbst lediglich auf die Anzeige von Projektdaten und das Protokollieren der Benutzeraktionen.
Das bedeutet jedoch nicht, dass Präsentationskomponenten vollständig von jeder Logik entkleidet werden müssen.
Es als strenge Regel zu betrachten, führt lediglich dazu, dass das gleiche Problem in einer anderen Form wieder auftritt. Eine Tabelle kann zu Recht einen lokalen Interaktionszustand speichern, wie beispielsweise welche Zeilen erweitert sind oder welche Spalten sichtbar sind, da dieser Zustand tatsächlich zur Tabelle gehört.
Die bessere Leitidee lautet:
Lassen Sie Kenntnisse auf Infrastrukturseite nicht weiter den Komponentenbaum hinabwandern, als es die jeweilige Funktionalität tatsächlich benötigt.
Es geht nicht darum, die Logik aus den Komponenten vollständig zu entfernen.
Sondern darum, die Logik in der Nähe der Verantwortungseinheit zu halten, der diese Logik ihre Bedeutung verleiht.
5. Teile zusammensetzen anstelle von Optionen umschalten
Einer der auffälligsten Wandel trat ein, als wir aufhörten, jede neue Anforderung mit einer weiteren Eigenschaft zu beantworten.
Generische Komponenten neigen dazu, durch Konfiguration zu wachsen.
Eine Tabelle kann zunächst bescheiden aussehen:
<DataTable rows={projects} columns={columns} />
Dann häufen sich die Anforderungen:
<DataTable
rows={projects}
columns={columns}
selectable
sortable
paginated
editableRows
showBulkActions
enableExport
customToolbar={toolbar}
rowActions={rowActions}
emptyState={emptyState}
onSelectionChange={handleSelection}
/>
Keine dieser Eigenschaften ist für sich genommen unangemessen.
Das Problem beginnt erst, wenn die Komponente nachverfolgen muss, welche Kombinationen von Eigenschaften überhaupt gültig sind.
Können Zeilen gleichzeitig bearbeitbar und auswählbar sein?
Sollten Massenaktionen angezeigt werden, wenn die Exportfunktion deaktiviert ist?
Ersetzt eine benutzerdefinierte Werkzeugleiste die Standardwerkzeugleiste oder steht sie daneben?
Wird die Seitenleiste auf der Client-Seite oder vom Server gehandhabt?
Jeder hinzugefügte Boolesche Wert und Callback erhöht die Anzahl der Zustände, die das generische Komponentenmodell berücksichtigen muss.
Durch Komposition wird ein Teil dieser Verantwortung wieder auf den Aufrufer übertragen:
<Table>
<TableToolbar>
<ProjectFilters />
<ExportButton />
</TableToolbar>
<ProjectRows projects={projects} />
<Pagination />
</Table>
Nun entscheidet der Elternteil, welche Komponenten für diesen spezifischen Workflow tatsächlich benötigt werden.
Die grundlegenden Tabellendatenstrukturen müssen nicht bereits alle produktbezogenen Variationen berücksichtigen, die die Anwendung irgendwann entwickeln könnte.
Die Konfiguration erwartet, dass eine Komponente jede mögliche Kombination vorherseht. Durch Komposition kann der Aufrufer nur die Kombinationen erstellen, die er tatsächlich benötigt.
Komposition verhindert nicht automatisch, dass jemand etwas Sinnloses zusammenstellt. Der Aufrufer kann weiterhin fehlerhafte Kombinationen erzeugen.
Was sich ändert, ist etwas anderes: Der gemeinsam genutzte Komponente muss nicht mehr jede für das jeweilige Unternehmen spezifische Variante selbst kodieren.
Einige Konfigurationen sind weiterhin völlig in Ordnung. Eine wiederverwendbare Button-Komponente sollte beispielsweise konsistente Optionen wie Größe, deaktivierter Zustand oder visuelle Hervorhebung unterstützen.
Die eigentliche Frage ist, ob eine bestimmte Eigenschaft eine weitere Variante derselben zugrunde liegenden Aufgabe darstellt oder ob sie einer Komponente heimlich beibringt, gleichzeitig mehrere unzusammenhängende Aufgaben zu bewältigen.
6. Klare Zuständigkeitsverteilung macht Entscheidungen bezüglich des Zustands einfacher
Die meisten Probleme mit dem Frontend-Zustand sehen auf den ersten Blick wie Probleme mit den Werkzeugen aus.
Die Diskussion dreht sich in der Regel um Mechanismen:
Soll dies im Komponentenzustand, im Context, in Zustand, bei Redux, in der URL oder im Server-Cache gespeichert werden?
In der Praxis lösen sich viele dieser Probleme jedoch von selbst, sobald man weiß, wer tatsächlich für das Verhalten verantwortlich ist.
Der Zustand neigt dazu, anzuwachsen, wenn die Verantwortlichkeiten unklar sind:
Ein Modal-Element beginnt mit einem Zustand, der in ihm selbst liegt.
Dann muss auch ein anderes Komponente es auslösen, wodurch der Zustand übertragen wird.
Später benötigt auch eine Toolbar, die weiter entfernt im Aufbau des Systems liegt, Zugriff darauf, weshalb der Zustand in den Kontext übertragen wird.
Bald schon enthält ein einziger globaler Speicher Flaggen zur Anzeigebereitschaft von Modalen, unfertige Formulardruckfassungen, Tabelleinstellungen für Filter, in Cache gespeicherte API-Antworten, aktuelle Tabauswahlen sowie verschiedene Details von Funktionen, die miteinander nichts zu tun haben.
Zu diesem Zeitpunkt geben die Leute in der Regel der Zustandsverwaltungs-Bibliothek die Schuld für das Chaos.
Doch das eigentliche Problem liegt meist nicht im Tool, sondern bei den Verantwortlichkeiten.
Sobald die Grenzen der Komponenten bereits die tatsächlichen Verantwortlichkeiten widerspiegeln, hat der Zustand natürlicherweise einen sinnvollen Platz, an dem er gespeichert werden kann.
Alles, was ein Benutzer derzeit in ein Feld eingibt, kann dort lokal bleiben.
Ein mehrstufiger Stornoverlauf gehört zur Funktion zum Stornieren.
Alles, was vom Server abgerufen wird, gehört dorthin, wo Ihre Anwendung die Aspekte des Serverzustands verarbeitet.
Ein Filter, der über Navigation hinweg beibehalten oder per Link geteilt werden muss, gehört vermutlich in die URL.
Zustände, die tatsächlich über viele unzusammenhängende Teile der Anwendung geteilt werden, rechtfertigen möglicherweise einen umfassenderen, zentraleren Verantwortlichen.
Nichts davon lässt die schwierigen Entscheidungen bezüglich des Zustands verschwinden, aber es bietet Ihnen einen Rahmen dafür.
Sobald die Grenzen der Komponenten die tatsächliche Verantwortung widerspiegeln, wird es viel einfacher, zu entscheiden, wo der Zustand gespeichert werden sollte.
Dieses mentale Modell hat dem Team mehr genützt als jede Auswahl einer „richtigen“ State Library je könnte.
7. Manchmal war Duplizierung die bessere Lösung
Der schwierigste Teil dieses Ansatzes war zu akzeptieren, dass eine gewisse Menge an Duplizierung in Ordnung ist – sogar vorteilhaft.
Stellen Sie sich zwei Formulare vor, die fast identisch aussehen und etwa 80 Prozent ihrer Markup-Struktur teilen.
Der Instinkt sagt einem, sie sofort in ein gemeinsames Komponenten zu verschmelzen.
Doch die verbleibenden 20 Prozent könnten völlig unterschiedliche Geschäftslogiken enthalten.
Möglicherweise validiert eines der Formulare anders.
Möglicherweise löst das Einreichen eines Formulars eine andere Abfolge von Effekten aus als das andere.
Eines könnte lediglich einen Entwurf speichern.
Das andere könnte etwas Unumkehrbares auslösen.
Eines der Formulare wird mit der Zeit wahrscheinlich komplexer, während das andere minimal bleiben sollte.
Der Versuch, beides in eine einzige gemeinsame Implementierung zu zwingen, führt oft dazu, dass etwas entsteht, das wie ein Labyrinth aus Bedingungen und Sonderfällen in einem „wiederverwendbaren“ Komponenten aussieht.
Dann hat man duplizierte JSX-Code gegen eine erhöhte kognitive Belastung eingetauscht. Jeder, der nun einen dieser Workflows bearbeitet, muss erst herausfinden, wie die gemeinsame Komponente die andere schützt, bevor er etwas sicher ändern kann.
In solchen Fällen ist es oft besser, einfach zwei getrennte, benannte Komponenten beizubehalten:
FeatureAForm
FeatureBForm
obwohl sich ein Teil des zugrundeliegenden Codes wiederholt.
Das ist kein Argument dafür, Duplikation im Allgemeinen als Vorteil zu betrachten.
Sobald eine wirklich gemeinsame Verantwortung offensichtlich und stabil wird, kann man sie extrahieren. Beide Formen könnten letztendlich dieselben Eingabeprimitiven, Validierungs-Hilfsmittel, Layout-Wrapper oder andere domänunabhängige Elemente teilen.
Der eigentliche Unterschied liegt im Zeitpunkt, nicht im Prinzip.
Anstatt sofort zu einer Abstraktion zu greifen, sobald zwei Dinge ähnlich aussehen, machte es mehr Sinn, zu warten, bis sich eine gemeinsame Grenze im Laufe der Zeit tatsächlich bewährt hat.
Daraus entstand eine Faustregel:
Es ist besser, einfache Code zu duplizieren, anstatt Logik zu teilen, die auf noch entwickelnden Annahmen beruht.
Einfach duplizierter Code ist leicht zu erkennen und bleibt an einem Ort begrenzt.
Eine zu früh erstellte Abstraktion hingegen kann mehrere für bestimmte Funktionen geltende Annahmen unter einem täuschend ordentlichen Namen zusammenfassen und so die Komplexität verbergen, die man eigentlich beseitigen wollte.
8. Was es wirklich bedeutet: Änderungen lokal halten
Schließlich wurde klar, dass dieser gesamte Ansatz nie wirklich darum ging, wie groß oder wiederverwendbar ein Komponent war.
Es ging darum, wie lokalisiert der Wandel sein kann.
Die Frage, die man jedes Mal stellen sollte, lautete:
Wenn wir eine Funktion ändern, wie viel unverwandten Code müssen wir zunächst verstehen?
Ein Design mit mehr gemeinsamen, generalisierten Komponenten weist technisch gesehen möglicherweise weniger duplizierte Zeilen auf als eines mit mehr funktionsspezifischen Elementen.
Aber es kann den Entwickler dennoch zwingen, einen weitaus größeren Teil der gesamten Anwendung im Kopf zu behalten, nur um eine sichere Änderung vorzunehmen.
Ein solcher Aufwand zeigt sich selten in irgendwelchen Wiederverwendungsmetriken.
Es kann ein wunderbar wiederverwendbarer Komponenten geben, der dennoch eine schreckliche Grenze für Änderungen darstellt.
Umgekehrt kann ein Komponenten, der ausschließlich für eine bestimmte Funktion entwickelt wurde und nur an einem Ort verwendet wird, die Wartbarkeit der Codebasis dennoch verbessern – einfach weil er diesen einen Arbeitsablauf abgeschlossen und leicht verständlich hält.
Das erwies sich als die präziseste Formulierung der gesamten Idee:
Entwerfen Sie Komponentengrenzen um lokale Logik, nicht um maximale Wiederverwendbarkeit.
Dieses eine Prinzip verbindet alles Weitere miteinander.
Wiederverwendbare Primitiven erhalten ihren Platz, weil sich Präsentationsmuster tatsächlich in einer Codebasis wiederholen.
Funktionsbezogene Komponenten bleiben spezifisch, weil Geschäftsprozesse aus Gründen ändern, die selten miteinander übereinstimmen.
Durch Komposition müssen gemeinsam genutzte Komponenten nicht jede mögliche Geschäftsvariante erlernen.
Der Zustand bleibt dort, wo das jeweilige Verhalten ihn tatsächlich steuert.
Die Duplikation wird erst dann akzeptabel, wenn das Teilen von Code Annahmen miteinander verbinden würde, die ohnehin bereits auseinanderdriften.
Die Frontend-Struktur wird einfacher – nicht weil jede Komponente kleiner oder wiederverwendbarer geworden ist, sondern weil jede von ihnen weniger vom Leser verlangt.
Die richtige Komponentengrenze ist eine, die man erklären kann, wenn sich etwas ändert
Es ist verlockend, eine Frontend-Codebasis anhand oberflächlicher Kriterien zu beurteilen: starke Wiederverwendung von Komponenten, minimale Duplikation, ordentlich strukturierte Ordner, kleine Dateigrößen. Solche Aspekte sind nicht bedeutungslos, doch sie sagen überraschend wenig darüber aus, was tatsächlich passiert, sobald sich eine Anforderung ändert.
Das erwies sich als der weitaus nützlichere Test, den man anwenden kann.
Wohin gehört dieses Verhaltensmerkmal eigentlich?
Welche Komponente ist für das Verstehen dieser Geschäftsregel verantwortlich?
Wo sollte dieser spezielle Codeabschnitt untergebracht werden?
Falls sich eine Anforderung ändert, welche Dateien sollten logischerweise zusammengefasst werden?
Welche Teile der Benutzeroberfläche sind tatsächlich wiederverwendbar und welche sollen ausschließlich einer bestimmten Funktion dienen?
Das Muster, das letztendlich diese Frontend-Struktur vereinfachte, war keine besonders raffinierte neue Abstraktionsmethode.
Es kam darauf an, jeder Schicht der Codebasis weniger zu verwalten.
Wiederverwendbare Primitiven waren für die Darstellung zuständig.
Funktionskomponenten kümmerten sich um die Abläufe.
Kompositionsgrenzen bestimmten, wie die Funktionen miteinander verbunden sind.
Und an Geschäftsbedingungen gebundene Komponenten durften einfach genau das bleiben: spezifisch.
Die resultierende Codebasis hatte nicht unbedingt weniger Komponenten oder das theoretisch minimale Maß an Duplikaten.
Was es stattdessen gab, war eine Codebasis, in der ein Entwickler an einer einzigen Funktion arbeiten konnte, ohne zunächst die Architektur des gesamten Frontends im Kopf behalten zu müssen.
Das erwies sich als weitaus praktischere Definition von Einfachheit als das Zählen von Komponenten oder duplizierten Zeilen je gewesen war.
Aus Ihrer eigenen Erfahrung heraus: Trugen mehr wiederverwendbare Komponenten oder klarere Grenzen zwischen bestehenden Komponenten mehr dazu bei, Ihren Frontend zu wartbarer zu machen?
Verwandte Artikel
- React Prop-Drilling und God Components reduzieren — Lernen Sie sieben konkrete Refactoring-Muster, um überdimensionierte React-Komponenten durch die Isolierung von State, Datenabruf, Berechtigungen und Ladelogik statt nur durch das Aufteilen von Dateien zu strukturieren.