Startseite / Artikel / Warum Frontend-Abstraktionen heimlich zu technischem Schuldenberg werden.

Warum Frontend-Abstraktionen heimlich zu technischem Schuldenberg werden.

Erfahren Sie, warum vorzeitige Frontend-Abstraktionen versteckte Komplexität hervorrufen und wie Sie beurteilen können, ob gemeinsame Komponenten, Hooks oder Utilities tatsächlich entwickelt werden sollten.

2389 Wörter

In fast jeder Frontend-Codebasis kommt ein Moment, in dem es stillschweigend aufhört, eine Anwendung zu sein, und stattdessen zu einem Framework wird, das dazu dient, eine Anwendung zu unterstützen.

Man führt eine Komponentenbibliothek ein.

Dann kommt ein Design-System hinzu.

Anschließend wird eine Schicht zur Zustandsverwaltung hinzugefügt.

Dann taucht eine Abstraktion zur Datenerfassung auf.

Dann umhüllt ein benutzerdefinierter Hook diese Datenerfassungsabstraktion.

Dann schreibt jemand ein generisches Formularkomponente, das ein Konfigurationsobjekt entgegennimmt, das beschreibt, wie sich Formulare verhalten sollen.

Bald schon bedeutet das Ändern von etwas So Einfachem wie einer Schaltfläche, sechs Dateien, drei Abstraktions-Ebenen sowie Konventionen durchforsten zu müssen, deren Urheber niemand mehr kennt.

Merkwürdigerweise schienen keine dieser einzelnen Entscheidungen zu diesem Zeitpunkt unvernünftig zu sein.

Das ist tatsächlich das Kernproblem bei der Art und Weise, wie Frontend-Teams mit Abstraktion umgehen.

Die meisten Abstraktionen sind an sich nicht schlecht, und viele helfen tatsächlich. Das Problem ist, dass Frontend-Entwickler erstaunlich geschickt darin geworden sind, Abstraktionen zu erstellen, bevor sie ausreichende Beweise dafür haben, dass diese Abstraktionen tatsächlich notwendig sind.

Wir abstrahieren nicht mehr nur die vorhandene Komplexität weg.

Wir abstrahieren sogar die bloße Möglichkeit weg, dass irgendwann Komplexität auftreten könnte.

Diese Gewohnheit führt zu einer besonderen Art von technischem Schuldenberg.

Die Abstraktion beginnt in der Regel mit guten Absichten

Stellen Sie sich drei separate Komponenten vor, von denen jede für das Abrufen von Benutzerdaten zuständig ist.

Die erste enthält etwas duplizierte Ladelogik.

Die zweite wiederholt fast dasselbe Muster.

Die dritte macht wieder etwas leicht Anderes.

Jemand bemerkt die Wiederholungen und schlägt vor:

"Wir sollten das wohl in eine gemeinsame Abstraktion überführen."

Das ist an sich eine berechtigte Beobachtung.

Daher entwickelt das Team einen benutzerdefinierten Hook:

const { data, loading, error } = useUserData(userId);

Sehr ordentlich und übersichtlich.

Dann benötigt eine andere Komponente eine kleine Anpassung im Verhalten.

Anstatt direkt die API zu nutzen, fügt das Team eine weitere Option hinzu:

useUserData(userId, {
  includePermissions: true,
  cache: true,
  retry: 3,
});

Einige Monate später lädt der Hook nicht mehr nur Benutzerdaten herunter.

Er kümmert sich nun um Caching, Wiederholungsversuche, Berechtigkeitsprüfungen, Datenumwandlungen, Fehlernormalisierung, optimistische Aktualisierungen sowie verschiedene feature-spezifische Besonderheiten.

Die ursprüngliche Duplikation ist verschwunden.

Aber etwas anderes hat sich eingeschlichen: eine wachsende Kluft zwischen dem Code und dem, was er tatsächlich tut.

Die Betrachtung einer Komponente sagt einem nicht mehr, woher ihre Daten stammen.

Zuerst muss man die Abstraktion verstehen, die dahintersteckt.

Das ist der Kompromiss, den niemand anspricht.

Abstraktion beseitigt die Komplexität nicht.

Sie verschiebt sie lediglich.

Mannchmal ist diese Verschiebung tatsächlich sinnvoll.

Andernfalls tauscht man einfach fünf Zeilen einfacher Logik gegen dreihundert Zeilen interner Mechanismen ein.

Abstraktion hat ihren Preis

Entwickler lernen früh, Duplikationen als Nachteil zu betrachten.

Das ist durchaus berechtigt, denn oft sind sie tatsächlich ein Problem.

Aber Duplikationen sind bei weitem nicht die einzige Form von Komplexität.

Weitere Formen sind:

  • Indirektheit
  • Konfiguration
  • Implizites Verhalten
  • Generische APIs
  • Versteckte Abhängigkeiten
  • Konventionen
  • Vererbung
  • Wrapper-Komponenten
  • Debugging, das nur aufgrund der Abstraktion selbst existiert
  • Kognitiver Aufwand

Manche Duplikationen sind tatsächlich günstiger als eine aufwendige Abstraktion.

Vergleichen Sie zwei Ansätze.

Einer wiederholt einen kleinen Teil der Logik dreimal.

Der andere erstellt eine generische Hilfsfunktion mit einem Dutzend Parametern – begründet damit, dass drei der aktuellen Anwendungsfälle etwa zu 60 Prozent übereinanderfallen.

Die zweite Option wirkt polierter, eher „ingenieurtechnisch“ gestaltet.

Aber sie kann auch erheblich schwieriger in der Wartung sein.

Dabei geraten Frontend-Entwicklungen regelmäßig ins Straucheln.

Teams optimieren lieber nach dem Prinzip des DRY-Prinzips statt danach, ob der Code leicht verständlich ist.

Diese Ziele sind nicht austauschbar.

Das Frontend-Ecosystem fördert dies

Die Frontend-Entwicklung hat eine besondere Beziehung zur Abstraktion – hauptsächlich, weil das gesamte Ökosystem von vornherein schichtweise aufgebaut ist.

Eine einzelne moderne Anwendung kann mühelos ein Rendering-Framework zum Erstellen von Komponenten, eine Art Routing-Bibliothek, einen State-Manager auf der Client-Seite, ein separates Tool zum Verwalten des Server-State sowie eine Form-Bibliothek mit eigenem Validierungs-Paket einbinden. Darüber hinaus gibt es ein Design-System, eine Sammlung vorgefertigter UI-Komponenten, eine über normales CSS gelegte Styling-Abstraktion, ein Build-Tool, das alles koordiniert, sowie ein Testing-Framework, das die gesamte Konfiguration überwacht.

Jedes dieser Elemente löst für sich genommen ein reales Problem.

Probleme entstehen, sobald die Anwendung eigene, benutzerdefinierte Schichten auf all das stapelt.

Anstelle des direkten Einsatzes des Frameworks greift ein Entwickler auf einen internen Wrapper darum zurück.

Der Wrapper wiederum ist von einem weiteren Wrapper abhängig.

Bald ähnelt das tägliche Programmiermodell des Teams kaum noch der zugrunde liegenden Plattform.

Dieses Muster tritt besonders häufig in größeren Organisationen auf.

Ein Team könnte schließlich etwas wie Folgendes entwickeln:

<AppPage>
  <DataBoundary>
    <PermissionGate>
      <FormContainer>
        <EntityEditor />
      </FormContainer>
    </PermissionGate>
  </DataBoundary>
</AppPage>

Jeder Bestandteil hat ein definiertes Ziel.

Jede Schicht hat einen dokumentierten Grund für ihre Existenz.

Sobald jedoch etwas ausfällt, muss ein Entwickler mental den gesamten Stack neu aufbauen, bevor er überhaupt zum eigentlichen Funktionscode gelangt.

Diese Neuaufbauarbeit ist weder kognitiv noch zeitlich kostenlos.

Generische Komponenten sind oft die größten Übeltäter

Ein Weg zur Komplexität im Frontend ist es, eine Komponente zu entwerfen, die alle zukünftigen Anforderungen antizipieren soll.

Es beginnt unschuldig:

<Button />

Dann entwickelt es sich weiter:

<Button variant="primary" />

Und wächst stetig weiter:

<Button
  variant="primary"
  size="large"
  loading
  icon={...}
  permission="admin"
  analyticsEvent="save"
  confirm
/>

Irgendwann bleibt etwas übrig, das technisch gesehen nicht mehr ein Button ist.

Es handelt sich um ein Miniatur-Framework zur Darstellung beliebiger Aktionen.

Das Team fühlt sich produktiver, da neue Buttons nun durch Konfiguration statt durch eine neuartige Implementierung erstellt werden können.

Aber Konfiguration ist trotzdem Code – egal, wie sie auch aussehen mag.

In gewisser Weise ist es sogar schlimmer, denn die Konfiguration verdeckt den eigentlichen Ablauf.

Liest man zwanzig Zeilen einfachen Codes, kann man genau nachvollziehen, was passiert.

Liest man zwanzig Zeilen Konfiguration, muss man oft in die Implementierung des Komponenten eintauchen, den Konfigurationsparser durchgehen, Standardwerte herausfinden und herausstellen, welche Optionen miteinander interagieren.

Eindeutiger Code wurde im Grunde durch ein Wortschatzsystem ersetzt.

Ein solches Wortschatz kann tatsächlich sehr mächtig sein.

Aber er kann genauso leicht zu einem Dialekt werden, den niemand weiter pflegen möchte.

Die „Zukunftssichere“ Falle

Zukunftssicherheit ist in der Regel die stärkste Begründung, die jemand für das Hinzufügen einer Abstraktion anführt.

„Wir könnten das später brauchen.“

„Wahrscheinlich werden es im Laufe der Zeit weitere Varianten geben.“

„Das könnte anderswo in der Anwendung wiederverwendet werden.“

„Lassen Sie uns es von Anfang an generisch erstellen.“

Mannchmal ist dieser Instinkt richtig. Weitaus öfter hat man einfach noch nicht genügend Informationen, um das beurteilen zu können.

Das Problem ist, dass jede Abstraktion Voraussetzungen bezüglich des Problems mit einschließt. Zu früh abstrahieren bedeutet, Architekturentscheidungen festzulegen, bevor man wirklich versteht, was man baut. Und sobald andere Teile der Codebasis von dieser Abstraktion abhängig werden, wird es schnell sehr teuer, den Kurs zu ändern.

Deshalb ist frühzeitige Abstraktion gefährlicher, als es auf den ersten Blick scheint. Duplizierter Code lässt sich in der Regel leicht refaktorieren, sobald man bereit ist. Eine fehlerhafte Abstraktion hingegen neigt dazu, sich weiter auszubreiten.

Stellen Sie sich drei ähnlich aussehende Komponenten vor. Man könnte die Duplizierung vorerst unberührt lassen. Wenn im Laufe der Zeit ein echtes gemeinsames Muster entsteht, kann man es dann extrahieren. Wenn man jedoch direkt zu einer generalisierten Abstraktion übergeht, muss jeder zukünftige Anwendungsfall den anfänglichen Voraussetzungen angepasst werden.

Zu diesem Zeitpunkt ist die Abstraktion nicht mehr nur eine Erleichterung, sondern wird zu einer Einschränkung. Das, was dazu dienen sollte, Wiederholungen zu beseitigen, macht die Änderung letztendlich schwieriger als die ursprüngliche Duplikation es je getan hätte.

Gute Abstraktionen entstehen meist aus Notwendigkeit

Die stärksten Abstraktionen werden in der Regel nicht im Voraus geplant. Sie werden entdeckt.

Ein Team implementiert dasselbe Verhalten mehrfach. Irgendwann erkennen sie, welche Teile tatsächlich identisch sind und welche nur oberflächlich ähnlich aussehen. Sie verstehen, was wirklich unterschiedlich ist. Erst dann isolieren sie den stabilen, gemeinsamen Kern.

Dadurch entsteht eine viel solideere Grundlage. Die Abfolge lässt sich wie folgt beschreiben:

Duplikation führt zu Wiederholungen, Wiederholungen führen zum Verständnis und das Verständnis führt zur Abstraktion.

Frontend-Teams folgen jedoch oft einem anderen Weg:

Möglichkeiten führen direkt zur Abstraktion, dann zur Konfiguration und schließlich zu Verwirrung.

Der erste Ansatz erfordert zunächst mehr Zeit. Im Laufe des gesamten Projekts ist er jedoch in der Regel schneller, weil die entstehende Abstraktion das Wissen widerspiegelt, das das Team durch Erfahrung tatsächlich erworben hat.

Nicht alle Duplikate sollten entfernt werden

Das ist eine unangenehme Wahrheit für Entwickler, da Duplikate von vornherein meist wie Fehler wahrgenommen werden. Doch manchmal ist eine Duplikation die richtige Entscheidung.

Nehmen wir an, zwei Komponenten teilen eine nahezu identische Validierungslogik. Wenn diese Logik nur aus fünf Zeilen besteht und jede Komponente anderen Geschäftsregeln unterliegt, kann es klüger sein, den Code unverändert zu belassen.

Warum? Weil ihre Trennung das lokale Verständnis bewahrt. Jemand, der später an einem Komponenten arbeitet, kann ihr Verhalten anpassen, ohne befürchten zu müssen, versehentlich die andere Komponente zu beeinträchtigen.

Duplizierter Code signalisiert implizit: Diese beiden Dinge ähneln sich im Moment zufällig.

Eine Abstraktion besagt etwas Weitaus Stärkeres: Diese beiden Dinge sollen identisch sein und sollten künftig gemeinsam verändert werden.

Das ist eine größere Behauptung. Man sollte nur auf eine Abstraktion zurückgreifen, wenn man wirklich glaubt, dass diese Behauptung zutrifft.

Abstraktionen sollten eine kleine API haben

Eine nützliche praktische Überprüfung ist folgende: Wie viel muss man tatsächlich lernen, bevor man diese Abstraktion richtig verwenden kann?

Falls diese Liste weiter wächst, verwandelt sich die Abstraktion vermutlich in ein Framework.

Eine gute Abstraktion verbirgt die Komplexität. Eine schlechte versteckt Entscheidungen. Das klingt ähnlich, ist aber überhaupt nicht dasselbe.

Eine gut gestaltete API kann genauso einfach aussehen wie diese:

const user = useUser(id);

Eine weniger solide Version fängt an, immer mehr Flags und Optionen hinzuzufügen, bis sie so aussieht wie diese:

const user = useUser(id, {
  cache: true,
  normalize: true,
  permissions: true,
  optimistic: false,
  retry: 3,
  suspense: false,
  transform: customTransform,
  mode: "editor",
});

Irgendwann hört die Abstraktion auf, etwas zu vereinfachen. Sie verschiebt das ursprüngliche Problem lediglich in ein Konfigurationsobjekt. Dieser Wandel sollte als Warnsignal betrachtet werden.

Frontend-Teams benötigen ein Abstraktionsbudget

Teams sprechen bereits regelmäßig über Leistungsbudgets, Budgets für die Größe von Paketen, Fehlerbudgets und Infrastrukturbudgets. Es lohnt sich, auch ein Abstraktionsbudget zu dieser Liste hinzuzufügen.

Ziel ist nicht unbedingt eine geringere Anzahl an Abstraktionen im absoluten Sinne. Vielmehr soll die Anzahl der Abstraktionen innerhalb dessen bleiben, was das Team bequem im Kopf behalten kann.

Vor dem Hinzufügen eines neuen Konzepts hilft es, sich einige Fragen zu stellen.

Wie oft ist dieses genaue Muster bereits aufgetreten? Wenn die Antwort einmal lautet, abstrahieren Sie es noch nicht. Bei zwei Auftritten sollte dies eher als Grund zur Vorsicht denn zu einer Handlung angesehen werden. Fünf Auftritte deuten bereits auf echte Beweise hin.

Nächster Schritt: Verändern sich diese Aspekte tatsächlich aus denselben Gründen? Ein ähnliches Erscheinungsbild reicht nicht als ausreichende Begründung aus. Zwei Komponenten können fast identisch erscheinen, während sie aus unterschiedlichen geschäftlichen Gründen völlig separate Entwicklungswege nehmen – in diesem Fall könnte es ein Fehler sein, sie zu einer einzigen Abstraktion zusammenzufassen.

Zuletzt: Macht diese Abstraktion den alltäglichen Einsatz tatsächlich einfacher? Nicht irgendeinen hypothetischen zukünftigen Fall – sondern den Fall, mit dem die Menschen ständig konfrontiert sind. Wenn das Verwenden der Abstraktion bedeutet, jedes Mal ihre Dokumentation erneut durchzulesen, haben Sie wahrscheinlich ein Problem gegen ein schlimmeres eingetauscht.

Der beste Frontend-Code ist oft langweilig

In der Softwareentwicklung gibt es eine eigenartige Statushierarchie. Eine clevere Abstraktion wirkt beeindruckender als drei einfache Komponenten. Ein generisches System erscheint architektonisch ernsthafter als eine einfache Funktion. Ein wiederverwendbares Framework wirkt professioneller als etwas duplizierter Code.

Doch Produktivsysteme belohnen keinen Code, der nur aufwendig aussieht. Sie belohnen Code, den Entwickler sicher modifizieren können.

Der wertvollste Frontend-Code ist in der Regel die langweilige Variante. Wenn man eine Komponente öffnet, kann man sofort erkennen, woher ihre Daten stammen. Man sieht genau, was ausgelöst wird, wenn ein Benutzer auf etwas klickt. Man kann die Markup-Struktur direkt bearbeiten, den Zustandsfluss nachverfolgen und die API-Aufrufe ohne Schwierigkeiten finden.

Man sollte nicht gezwungen sein, die interne Designphilosophie eines Teams zu erlernen, nur um einen Fehler zu beheben. Das ist kein Zeichen für unzureichende Ingenieurskunst – es ist ein Zeichen für gute Ingenieurskunst.

Abstraktion sollte das Denken reduzieren, nicht erhöhen

Abstraktion existiert nicht dazu, Code wiederverwendbar aussehen zu lassen. Ihr Zweck ist es, ein System verständlicher zu machen. Das sollte die Messlatte sein, an der Frontend-Teams jede Abstraktion messen sollten.

Falls eine Abstraktion es zehn Komponenten ermöglicht, komplexe Verhaltensweisen zu teilen, ohne dass jeder Entwickler verstehen muss, wie diese Verhaltensweisen implementiert sind, dann erfüllt sie ihren Zweck. Wenn hingegen jeder Entwickler eine komplexe API lernen muss, nur um an einer einfachen Komponente zu feilen, arbeitet die Abstraktion wahrscheinlich gegen einen.

Dieser Unterschied ist wichtig, weil die Arbeit am Frontend von Natur aus bereits schwierig ist. Browser sind kompliziert. Benutzeroberflächen sind kompliziert. Das Synchronisieren des Zustands ist kompliziert. Barrierefreiheit ist kompliziert. Leistung ist kompliziert. Es gibt keinen Grund, all dem noch zusätzliche Komplexität hinzuzufügen, nur damit die Architektur auf dem Papier anspruchsvoller aussieht.

Die nächste Stufe der Frontend-Entwicklung wird wahrscheinlich nicht durch die Entdeckung einer weiteren Abstraktionsschicht entstehen. Sie wird dadurch erreicht, dass man besser lernt, wann man keine solche Schicht erstellen sollte.

Die herausragenden Ingenieure werden nicht unbedingt diejenigen sein, die das ausgefeilteste, wiederverwendbare Komponentensystem entwerfen können. Es werden diejenigen sein, die ein Problem betrachten und richtig beurteilen können, ob überhaupt ein System erforderlich ist.

Mannchmal ist die richtige Abstraktion eine einzelne Funktion. Manchmal handelt es sich um ein Komponente. Manchmal ist es einfach ein gut benanntes Modul. Und manchmal sind es fünf Zeilen duplizierten Codes, die jeder im Team sofort lesen und verstehen kann.

Diese Situationen voneinander zu unterscheiden, ist die eigentliche Fähigkeit, die es zu entwickeln gilt.

Verwandte Artikel

  • React-Designmuster: Von klassischem OOP bis zu modernen Hooks — Erklärt, wie klassische Softwaremuster wie Singleton, Factory und Observer in React Anwendung finden, sowie reaktions-spezifische Muster wie HOCs, Hooks und Kombinationskomponenten.
  • Das Verständnis der SOLID-Prinzipien anhand praktischer Codebeispiele — Dieser Leitfaden erläutert alle fünf SOLID-Prinzipien anhand konkreter Codebeispiele und zeigt, wie sie in echten Projekten sowie React-Anwendungen angewandt werden.
  • Sechs TypeScript-Techniken, die Typen in echte Fehlerprävention verwandeln — Erfahren Sie, wie satisfies, tagged Unions, never Checks, unknown, abgeleitete Typen und branded IDs dazu beitragen, dass TypeScript echte Fehler bereits zur Kompilierzeit statt in der Produktion aufspürt.