Startseite / Artikel / Zehn Architekturgewohnheiten, die Frontend-Codebasen jahrelang wartbar halten

Zehn Architekturgewohnheiten, die Frontend-Codebasen jahrelang wartbar halten

Erklärt strukturelle Gewohnheiten wie die Optimierung für Löschbarkeit, einen klaren Datenfluss sowie die Trennung der Geschäftslogik, die dazu beitragen, dass Codebasen auch nach jahrelangen Änderungen wartbar bleiben.

3870 Wörter

Jede Frontend-Codebasis, die lange genug überlebt, teilt sich irgendwann in zwei unterschiedliche Bereiche auf.

Den ersten kann man die Gefahrenzone nennen.

Dort herrscht ein chaotischer Mix aus State-Managern, notdürftig reparierten Lebenszyklus-Hooks, cleveren aber undurchsichtigen globalen Abstraktionen, halbfertigen Experimenten sowie Hilfsfunktionen, die vor Jahren geschrieben wurden und deren Funktionsweise heute niemand mehr vollständig erklären kann.

Niemand möchte sich dort aufhalten.

Wenn eine neue Funktionsanfrage in diesen Teil der Anwendung gelangt, bewertet das Team den Aufwand nicht danach, wie schwierig die eigentliche Arbeit ist.

Sondern danach, wie riskant es erscheint, an diesem Code zu arbeiten.

Eine Änderung, die eigentlich zwei Tage dauern sollte, wird zu einer zweiwöchigen Aufgabe, weil alle wissen, dass der größte Teil der Zeit in Regressionstests statt in die eigentliche Entwicklung fließen wird.

Dann gibt es noch den anderen Bereich.

Nennen wir ihn die stabile Grundlage.

Dies sind die Module, die bereits vor Jahren entwickelt wurden und stillschweigend mehrere Framework-Migrationen, Neugestaltungen, Richtungswechsel des Produkts sowie Änderungen in der Führungsebene überdauert haben.

Sie verursachen fast nie Ausfälle.

An der Art und Weise, wie sie geschrieben sind, ist nichts besonders Kluges.

Und wenn jemand Neues zum Team stößt, kann er eines dieser Dateien öffnen, verstehen, was sie tun, ohne eine Einweisung benötigen, und innerhalb von ein oder zwei Tagen einen ersten Pull Request einreichen.

Das ist der Aspekt, dem man Aufmerksamkeit schenken sollte.

Der Code, der lange hält, ist in der Regel nicht der fortschrittlichste Code im System.

Er ist meistens der einfachste.

Erfahrene Ingenieure wissen, dass Software niemals in den Bedingungen bleibt, unter denen sie ursprünglich entwickelt wurde.

Die Anforderungen ändern sich.

Teams werden umorganisiert.

Abhängigkeiten werden ausgetauscht.

Frameworks entwickeln sich weiter.

Unternehmen ändern ihre Strategie.

Menschen verlassen das Unternehmen.

Neue Ingenieure kommen ohne jegliches Verständnis dafür, warum Dinge auf bestimmte Weise konstruiert wurden.

Daher ist die richtige Frage nicht:

"Wie sauber sieht dieses Design im Moment aus?"

Sondern:

"Wie teuer wird es in fünf Jahren sein, das zu ändern?"

Die folgenden sind die strukturellen Gewohnheiten, die eine solche Langlebigkeit ermöglichen.

1. Optimieren Sie für Löschbarkeit, nicht für Wiederverwendbarkeit

Viele Leitlinien zur Architektur konzentrieren sich auf die Wiederverwendbarkeit.

Machen Sie Ihre Komponenten wiederverwendbar.

Bauen Sie generische Dienste.

Fügen Sie Erweiterungspunkte hinzu.

Entwerfen Sie Plugin-Architekturen.

Schreiben Sie Abstraktionen für Implementierungen, die Sie noch nicht erstellt haben.

Die Wiederverwendbarkeit hat ihren Platz.

Aber es gibt eine weitere Eigenschaft, die bei einem ständig weiterentwickelten Produkt oft wichtiger ist:

Wie einfach etwas lösbar ist.

Funktionen sind nicht dauerhaft.

Sie werden ausgetauscht.

Sie werden neu erstellt.

Sie werden in andere Funktionen integriert.

Mannchmal verliert das Unternehmen einfach das Interesse an ihnen.

Stellen Sie sich ein React-Projekt vor, das streng nach Dateitypen organisiert ist:

src/
  components/
    BillingTable.tsx
    UserModal.tsx
    SubscriptionCard.tsx
  hooks/
    useBillingData.ts
    useUserData.ts
    useSubscription.ts  services/
    billingApi.ts
    userApi.ts
    subscriptionApi.ts

Auf den ersten Blick scheint das ordentlich zu sein.

Jeder Dateityp hat seine eigene festgelegte Ordnerstruktur.

Aber nehmen wir an, das Unternehmen entscheidet sich nach achtzehn Monaten, den Zahlungsablauf komplett abzuschaffen.

Wo befindet sich dann eigentlich alles, was mit der Abrechnung zu tun hat?

Man müsste in mehreren verschiedenen Verzeichnissen suchen.

Man findet und löscht BillingTable.tsx.

Dann entdeckt man useBillingData.ts.

Dann einige Typdefinitionen, die speziell für die Rechnungsstellung geschrieben wurden.

Dann eine Hilfsfunktion, die ausschließlich von der Rechnungsstellung aufgerufen wird.

Dann ein Stylesheet.

Dann ein API-Aufruf.

Dann ein Test-Fixture.

Dann ein Hook, der ursprünglich als Rechnungsstellung-Hook konzipiert war, aber im Laufe der Zeit umbenannt wurde.

Die Funktion ist aus dem Produkt entfernt, doch Überreste davon sind weiterhin im Codebase verstreut.

Genau so ansammelt sich im Laufe der Zeit tote Code.

Durch Organisieren nach Funktionen werden die Grenzen deutlich sichtbarer:

src/
  features/
    billing/
      components/
        BillingTable.tsx
      hooks/
        useBillingData.ts
      services/
        billingApi.ts
      types.ts
      index.ts

Nun hat die Rechnungsstellung einen klaren Platz.

Falls das Unternehmen die Funktion abschafft, ist der erste Schritt einfach:

src/features/billing/

Löschen Sie den Ordner.

TypeScript zeigt anschließend alles an, was weiterhin darauf angewiesen ist.

Das ist eine viel gesündere Art von Abhängigkeit, umgehen zu müssen.

Löschbarkeit ist eine Form der Wartbarkeit

Ein Modul wird leichter zu warten, wenn man sofort erkennen kann, wo sich seine Verantwortlichkeiten befinden.

Deshalb tauchen strukturierte Ordnersysteme nach Funktionen immer wieder in Gesprächen über die Skalierung großer React-Codebasen auf. Teams, die an umfangreichen Anwendungen arbeiten, schlagen oft die Zusammenlegung von Inhalten vor, weil dies den Einflussbereich jeder einzelnen Änderung verringert.

Ziel ist es nicht, einen perfekt organisierten Verzeichnisbaum anzustreben.

Ziel ist vielmehr, schnell folgende Frage beantworten zu können:

„Falls diese Funktion morgen verschwinden würde, was müsste ich dann löschen?“

Falls diese Frage schwer zu beantworten ist, sind die Grenzen der Funktionen vermutlich zu vage.

2. Machen Sie aus shared/ keinen Müllordner

Es gibt eine weitere Falle, die oft auftaucht, sobald Teams auf eine architektonische Struktur nach Funktionen wechseln.

Alles, was offensichtlich nicht zu einer bestimmten Funktion gehört, landet in shared/.

Eine paar Monate später hat man etwas wie Folgendes:

shared/
  utils/
  helpers/
  common/
  services/
  components/
  hooks/
  types/

Und zu diesem Zeitpunkt macht shared/ still und heimlich die Hälfte der Codebasis aus.

Dies führt zu einem eigenen Art von Kopplungsproblem.

Eine gute Richtlinie lautet:

Code sollte in einen gemeinsamen Ordner verschoben werden, weil mehrere Funktionen tatsächlich auf dasselbe Konzept angewiesen sind – und nicht, weil man sich nicht entscheiden kann, wohin er sonst gehören soll.

Ein generisches Button-Komponenten passt natürlich in ein Design-System.

Ein Authentifizierungsclient könnte zu Recht in einer gemeinsamen Infrastruktursschicht untergebracht werden.

Auch ein Hilfsprogramm zur Formatierung von Datumsangaben könnte geteilt werden.

Aber eine Funktion wie:

calculateEnterpriseRenewalDiscount()

gehört fast sicher zu der Funktion, die diese spezifische Geschäftsregel verwaltet.

Wehren Sie sich dagegen, Dinge ausschließlich dazu in globale Ordner zu verschieben, um den Verzeichnisbaum ordentlicher aussehen zu lassen.

Gemeinsam genutzter Code ist nicht kostenlos, denn jede Funktion, die ihn verwendet, wird zu einem potenziellen Abhängigkeitsfaktor.

Höher wird der Umfang von shared/, desto schwieriger wird es, herauszufinden, wer tatsächlich für ein bestimmtes Verhalten verantwortlich ist.

3. Erstellen Sie schützende Adapter für externe Abhängigkeiten

Es ist sehr wahrscheinlich, dass Ihre Anwendung lange Zeit weiterläuft, auch nachdem einige ihrer Abhängigkeiten entfernt wurden.

Heute könnten Sie auf Axios setzen und morgen auf den eingebauten fetch-Funktion wechseln.

Möglicherweise nutzen Sie derzeit einen Analyseanbieter, während Ihr Unternehmen in ein paar Jahren auf einen anderen wechselt.

Heute könnten Sie eine Authentifizierungsbibliothek integrieren, nur um später aufgrund neuer Sicherheitsanforderungen zu einer anderen überzugehen.

Die zerbrechliche Methode, Dinge zu bauen, besteht darin, diese externen Pakete direkt in Dutzende von Komponenten einzubinden.

import axios from 'axios';
import { trackMixpanelEvent } from 'mixpanel-browser';
export function CheckoutCard() {
  const handlePurchase = async () => {
    await axios.post('/api/checkout', payload);    trackMixpanelEvent('checkout_completed');
  };
}

Zu diesem Zeitpunkt hat Ihre UI-Schicht direkten Zugriff darauf, welchen HTTP-Client und welchen Analytics-Anbieter Sie verwenden. Wenn dieses Muster auf vierundvierzig Komponenten angewandt wird, ist das Austauschen eines Anbieters nicht mehr eine begrenzte Aufgabe – es wird zu einer Veränderung, die sich im gesamten Repository ausbreitet.

Eine Trennschicht sorgt dafür, dass Abhängigkeiten austauschbar bleiben. Zum Beispiel:

// src/shared/lib/analytics.ts
import mixpanel from 'mixpanel-browser';export const analytics = {
  trackCheckoutCompleted(
    orderId: string,
    amount: number
  ) {
    mixpanel.track('checkout_completed', {
      orderId,
      amount,
    });
  },
};

Die Komponente kommuniziert nun mit einem von Ihrer eigenen Anwendung definierten Konzept:

analytics.trackCheckoutCompleted(orderId, amount);

Sie hat keine Ahnung, ob unterhalb davon Mixpanel, PostHog, Segment oder ein anderes Tool liegt. Wenn sich der Anbieter ändert, kann der Vertrag, auf den Ihre Anwendung angewiesen ist, genau gleich bleiben.

Aber verallgemeinern Sie nicht alles

Dieser Punkt ist genauso wichtig wie der vorherige.

Die Einbettung einer Abhängigkeit in einen Adapter ist an sich keine gute Designentscheidung. Wenn man um jede kleine Bibliothek, die man verwendet, eine eigene Schnittstelle erstellt, kann man am Ende mehr Code schreiben als die Bibliothek selbst enthält.

Die eigentliche Frage lautet:

„Wäre es später teuer, diese Abhängigkeit zu ersetzen, oder wäre es riskant, sie unkontrolliert im Codebase verbreiten zu lassen?“

Falls die Antwort ja lautet, lohnt sich der Einsatz eines Adapters. Andernfalls ist es wahrscheinlich einfacher und ausreichend, die Abhängigkeit direkt aufzurufen.

Senior Engineering bedeutet nicht, Abstraktionen über alles zu legen, was man berührt. Es bedeutet, Grenzen genau dort zu setzen, wo das Auslassen einer solchen Grenze später Kosten verursachen würde.

4. Expliziter Datenfluss ist besser als Magie

Eine der schnellsten Möglichkeiten, eine Codebasis unübersichtlich zu machen, besteht darin, zu verschleiern, woher die Werte tatsächlich stammen.

Globale Ereignisemitter sind ein klassisches Beispiel dafür.

eventBus.emit('USER_UPDATED', {
  id: user.id,
});

Diese Zeile informiert nur darüber, dass ein Ereignis ausgelöst wurde. Sie sagt nichts darüber aus, wer darauf lauscht.

Man könnte die gesamte Codebasis durchsuchen und schließlich etwas Ähnliches finden:

eventBus.on('USER_UPDATED', handler);

Aber es könnten drei separate Zuhörer vorhanden sein. Einer davon wurde vielleicht vor zwei Jahren hinzugefügt. Ein anderer könnte den globalen Zustand verändern. Ein dritter könnte eine Analyseanfrage senden. Plötzlich ist das tatsächliche Verhalten, das durch die ursprüngliche Funktion ausgelöst wird, über die gesamte Anwendung verteilt anstatt an einem Ort zusammengefasst zu sein.

Vergleichen Sie das nun mit einem Vertrag, der ausdrücklich definiert ist:

interface UserCardProps {
  user: User;
  onUserRoleChange: (
    userId: string,
    newRole: Role
  ) => Promise<void>;
}

Hier deklariert das Komponente offen, welche Aktionen es unterstützt, und die übergeordnete Komponente deklariert offen, was geschieht, wenn eine dieser Aktionen ausgelöst wird. Der Datenfluss ist auf der Seite sichtbar.

Ja, das ist umständlicher als das Auslösen eines anonymen Events. Doch diese zusätzliche Umständlichkeit lohnt sich, da dadurch ein nachvollziehbarer Pfad durch den Code entsteht.

Wenn jemand, der mit der Komponente nicht vertraut ist, die Datei öffnet, sollte er in der Lage sein, drei Fragen zu beantworten, ohne im Rest des Repositoriums suchen zu müssen:

Woher stammen diese Daten?

In der Regel handelt es sich um Props, Route-Parameter, einen Hook oder eine klar definierte Datenzugriffsschicht.

Was kann sie ändern?

Eine sichtbare Funktionsaufruf, eine Mutation, eine Aktion oder ein expliziter Zustandsupdate.

Was passiert, wenn der Benutzer diese Aktion ausführt?

Eine direkte Aufrufung einer Funktion, deren Implementierung nachvollzogen werden kann.

Höchstens so viel Verhalten wie nötig zu verbergen, macht das gesamte System leichter nachvollziehbar.

5. Halten Sie die Geschäftslogik außerhalb der Framework-Lebenszyklen

Frameworks sind keine dauerhaften Konstrukte – das ist eine der zuverlässigsten Annahmen, die man bei der Arbeit am Frontend treffen kann.

Allein React hat bereits mehrere große Veränderungen durchgemacht. Klassische Komponenten sind in Vergessenheit geraten. Hooks haben die Strukturierung von Zustandslogik neu organisiert. Create React App wurde in vielen Projekten durch Tools wie Vite oder framework-eigene Lösungen ersetzt. Server-first Rendering sowie neuere Routing-Methoden haben die Art und Weise, wie Teams an der Datenerfassung arbeiten und wo die Grenzen einer Anwendung liegen, verändert.

Das Framework, das Sie derzeit verwenden, könnte in fünf Jahren völlig anders aussehen als das Standardframework. Ihre Geschäftsregeln müssen jedoch unabhängig davon weiterhin funktionieren.

Nehmen Sie die Steuerberechnung als Beispiel. Eine fragile Version versteckt die eigentliche Logik innerhalb eines React Hooks:

export function useTaxCalculator(
  cartItems: CartItem[]
) {
  const [tax, setTax] = useState(0);
  useEffect(() => {
    let calculated = 0;    // 60 lines of tax calculation,
    // rounding rules,
    // country logic,
    // exemptions...    setTax(calculated);
  }, [cartItems]);  return tax;
}

Durch diese Verknüpfung ist Ihre Steuerberechnung an React gebunden. Das Testen erfordert das Starten einer React-Umgebung. Der Aufruf von dort aus über eine Server-Aktion wird unpraktisch, der Ausführung in einem Web Worker genauso. Die Portierung auf ein anderes UI-Framework ist außerdem eine aufwändige Aufgabe.

Ein besseres Vorgehen trennt diese beiden Aspekte voneinander:

export function calculateTax(
  cartItems: CartItem[],
  countryCode: string
): number {
  // Pure business logic
  return totalTax;
}

Die React-Schicht ruft dann einfach darauf zu:

const tax = calculateTax(cartItems, countryCode);

Mit dieser Struktur hat die wirklich wichtige Logik keinerlei Kenntnis von React. Sie kann in jedem Umfeld ausgeführt werden, ist mit einfachen Unit-Tests überprüfbar, kann direkt von einem Serverprozess wiederverwendet werden und übersteht einen Wechsel auf ein anderes UI-Framework, ohne neu geschrieben zu werden.

Frameworks gehören an die äußeren Ränder

Eine hilfreiche Art, dies darzustellen, ist ein schichtweises Diagramm:

┌──────────────────────────────┐
│          UI Layer            │
│      React / Next.js         │
├──────────────────────────────┤
│       Application Logic      │
├──────────────────────────────┤
│        Domain Logic          │
│     Pure TypeScript          │
├──────────────────────────────┤
│       Infrastructure        │
│ APIs / DB / Vendors / SDKs   │
└──────────────────────────────┘

Je näher ein Codeabschnitt am Zentrum liegt, desto weniger sollte er von einem bestimmten Framework oder einer Vendor-Bibliothek abhängen.

Das bedeutet nicht, dass jedes React-Projekt eine vollständige „Clean Architecture“-Einstellung erfordert.

Das bedeutet vielmehr, dass man klar erkennen muss, welche Teile des Codes tatsächlich spezifisch für React sind und welche Teile die eigentlichen Geschäftsregeln darstellen.

Das sind zwei unterschiedliche Kategorien – wenn man sie als eine betrachtet, beginnen die Probleme.

6. Vermeiden Sie es, Hooks in Mini-Anwendungen zu verwandeln

Dieses Muster taucht immer wieder in React-Codebasen auf.

Es beginnt in der Regel unschuldig:

function useUser() {
  // fetch user
}

Dann häufen sich im Laufe der Zeit weitere Anforderungen an.

function useUser() {
  // fetch user
  // loading state  // error handling  // permissions  // analytics  // transformations  // caching  // retry logic  // business rules  // notifications  // feature flags
}

Bald schon ist aus einem einfachen Hook eine 500-Zeilen-Anwendung geworden, die sich hinter einem unscheinbaren Funktionsnamen verbirgt.

Hooks sind tatsächlich nützlich.

Aber ein Hook sollte nicht zum Auffangbecken für alle Probleme werden, nur weil er einfachen Zugriff auf den React-State und -Effects hat.

Ein besseres Vorgehen besteht darin, den Hook an kleinere, fokussierte Komponenten zu delegieren:

function useUser() {
  const user = useUserQuery();
  const permissions =
    calculatePermissions(user.data);  return {
    user: user.data,
    permissions,
    isLoading: user.isLoading,
  };
}

Durch diese Struktur wird der Hook zu einer Orchestrierungsschicht, die die verschiedenen Komponenten miteinander verbindet.

Es handelt sich nicht mehr um die gesamte Architektur, die in einer einzigen Funktion untergebracht ist.

Diese Grenze ist im Laufe der Zeit viel leichter aufrechtzuerhalten.

7. Erstellen Sie Architekturentscheidungsprotokolle – nicht endlose Wikis

Eine der häufigsten Ursachen für einen Verfall der Architektur liegt keineswegs im unübersichtlichen Code.

Sondern bei verlorenem Kontext.

So geschieht es in der Regel: Ein Entwickler trifft eine nicht offensichtliche Entscheidung. Die Entscheidung ist fundiert, und alle im Team verstehen den dahinterstehenden Grund. Danach wechselt diese Person zum nächsten Projekt.

Monate später stößt ein neuer Ingenieur auf diese ungewöhnliche Umsetzung und denkt:

"Warum machen wir das so? Es muss eine sauberere Lösung geben."

Daher schreibt er die Lösung um und führt unbewusst genau das Problem wieder ein, das die ursprüngliche Entscheidung vermeiden sollte.

Nehmen Sie als Beispiel ein Dashboard, das auf Server-Sent Events statt auf WebSockets basiert.

Ohne die Hintergrundgeschichte könnte ein neuer Entwickler zu Recht folgern:

"WebSockets sind der modernere Standard. Lassen Sie uns wechseln."

Doch das ursprüngliche Team hat möglicherweise ausdrücklich SSE gewählt, weil viele Unternehmenskunden hinter restriktiven Firmen-Proxy-Servern arbeiten, die WebSocket-Verbindungen falsch handhaben.

Diese Begründung ist unsichtbar, wenn man nur auf den Code selbst schaut.

Genau diese Lücke sollen Architekturentscheidungsprotokolle schließen.

Zum Beispiel:

# ADR 003: Use Server-Sent Events for Dashboard Feeds
## ContextOur dashboard requires real-time metric updates.We evaluated WebSockets and Server-Sent Events.## DecisionWe chose Server-Sent Events because:1. Communication is strictly server-to-client.
2. SSE uses standard HTTP infrastructure.
3. Browser reconnection is supported natively.
4. The solution works reliably within our enterprise network environment.## ConsequencesIf we later require client-to-server
bi-directional streaming, we should
re-evaluate this decision.

Mit einem solchen Protokoll muss der nächste Entwickler die Begründung nicht von Grund auf rekonstruieren.

Er kann sofort das Warum erkennen.

Einfach

/docs/adr/

Eine Dateiordner in Ihrem Repository kann jahrelange institutionelles Wissen bewahren, das sonst mit jedem verschwindet, der das Team verlässt.

Dokumentieren Sie Entscheidungen, nicht alles

Ihr müssen eine umfangreiche, hundertseitige Wiki verwalten.

In den meisten Fällen sollte der Code selbst klar genug sein, um zu erklären was er tut.

Die Dokumentation sollte stattdessen die Gründe festhalten, die der Code allein nicht ausdrücken kann:

  • warum eine bestimmte Technologie gewählt wurde
  • warum eine offensichtlichere Alternative abgelehnt wurde
  • warum es überhaupt eine ungewöhnliche Einschränkung gibt
  • warum ein scheinbar unnötiger Workaround tatsächlich noch erforderlich ist

Dokumentation ist dann wirklich nützlich, wenn sie Kontexte festhält, die sonst zusammen mit den Personen, die ihn kannten, verschwinden würden.

8. Design für den Ingenieur, der nach Ihnen kommt

Das könnte der einfachste Test dafür sein, ob eine Architektur langfristig Bestand hat.

Stellen Sie sich eine Situation vor, in der ab morgen alle, die denzeit die Funktionsweise des Systems verstehen, das Unternehmen sofort verlassen.

Könnte ein neues Team das System weiterhin betreiben?

Falls Ihre ehrliche Antwort Nein lautet, bedeutet das nicht unbedingt, dass es an Entwicklern mangelt. Es bedeutet vielmehr, dass das System von den Kenntnissen bestimmter Personen abhängig ist.

Eine Architektur, die langfristig bestehen soll, sollte es ermöglichen, ihr wichtiges Verhalten selbstständig zu erkennen.

Jemand Neues sollte in der Lage sein, das Repository zu öffnen und allmählich Antworten auf Fragen wie folgende zusammenzustellen:

  • Wo befindet sich diese spezielle Funktion im Codebase?
  • Welches Modul ist für dieses Verhalten verantwortlich?
  • Woher stammen tatsächlich diese Daten?
  • Welche externen Systeme setzt dieser Code voraus?
  • Welche Annahmen trifft dieser Code stillschweigend?
  • Warum wurde diese spezielle architektonische Entscheidung getroffen?
  • Was kann sicher geändert werden, ohne etwas anderes zu stören?
  • Deshalb sind explizite Grenzen so wichtig.

    Ein Neuzugang sollte nicht die gesamte Unternehmensgeschichte lernen müssen, nur um zu verstehen, was der Code tut.

    Die Codebasis selbst muss genügend von dieser Geschichte mit sich tragen.

    9. Halten Sie den Einflussbereich von Änderungen klein

    Eine gute Möglichkeit, eine Architektur zu bewerten, besteht darin, mitzuzählen, wie viele Dateien eine einfache Änderung dazu zwingt, anzufassen.

    Stellen Sie sich einen einfachen Funktionsantrag vor: Jemand bittet um eine Schaltfläche, mit der Nutzer den Rechnungsbericht als CSV-Datei exportieren können.

    In einem eng vernetzten System könnte die Erfüllung dieses Anliegens das Bearbeiten von Dateien bedeuten, die überall verteilt sind:

    components/
    hooks/
    services/
    utils/
    types/
    global state/
    shared helpers/
    

    Der Ingenieur muss möglicherweise Dutzende von Dateien bearbeiten, nur um einen einzigen Button zu veröffentlichen.

    Vergleichen Sie das mit einer ordnungsgemäß strukturierten Funktionsarchitektur:

    features/
      billing/
        components/
        hooks/
        services/
        utils/
    

    Hier kann derselbe Änderungsvorgang fast vollständig innerhalb des eigenen Verzeichnisses der Rechnungsfunktion bleiben.

    Das ist es, was die Leute meinen, wenn sie von einer Verringerung des Expansionsradius einer Änderung sprechen.

    Ein kleiner Expansionsradius bringt Folgendes mit sich:

    • Weniger Regressionen
    • Einfachere Code-Reviews
    • Schnellere Lieferung
    • Weniger Merge-Konflikte
    • Einfachere Tests
    • Sicherere Refaktorisierungen

    Dafür ist keine ausgefeilte Architektur notwendig – es reichen Grenzen, die tatsächlich der Entwicklung des Produkts in der Praxis entsprechen.

    10. Hören Sie auf, sich nur nach dem Architekturdiagramm zu optimieren

    Auch ein wunderschönes Architekturdiagramm kann eine Codebasis verbergen, in der es äußerst schwierig ist zu arbeiten.

    Sie könnten jedes dieser Kästchen ankreuzen:

    • die Einhaltung eines Clean Architecture-Layouts
    • Anwendung der SOLID-Entwurfsprinzipien
    • richtige Umkehrung der Abhängigkeiten
    • Einschließung des Datenzugriffs in Repository-Muster
    • Erzeugung von Objekten mittels Factory-Muster
    • Vernetzung der Komponenten über Ereignisse
    • Aufbau mehrerer Abstraktions-Ebenen übereinander

    und dennoch eine einfache Funktion in einen mehrtägigen Aufwand verwandeln.

    Architektur soll Komplexität verringern, nicht erhöhen. Wenn die architektonische Schicht mehr Konzepte einführt als das eigentliche Produkt, ist etwas schiefgelaufen.

    Oft ist die beste Architektur jene, über die niemand spricht, weil Entwickler den Code einfach lesen und verstehen können. In der Praxis könnte das so aussehen:

    features/
      billing/
      checkout/
      accounts/
    

    in Kombination mit etwas Bescheidenem:

    shared/
      ui/
      lib/
    

    plus ein paar reine Funktionen zur Geschäftslogik.

    Das klingt alles nicht besonders beeindruckend. Doch wenn es nach vier Jahren immer noch funktioniert und sinnvoll ist, erfüllt es genau das, wofür Architektur da sein soll.

    Wofür optimieren erfahrene Ingenieure tatsächlich

    Erfahrene Ingenieure erzeugen nicht unbedingt komplexeren Code. Was sich unterscheidet, ist die Reihe von Fragen, die sie vor dem Schreiben stellen.

    Ein weniger erfahrener Ingenieur könnte fragen:

    "Wie mache ich das wiederverwendbar?"

    Ein erfahrener Ingenieur fragt hingegen:

    "Muss das überhaupt wiederverwendbar sein?"

    Ein weniger erfahrener Ingenieur könnte fragen:

    "Wie sollte ich das abstrahieren?"

    Ein erfahrener Ingenieur fragt hingegen:

    "Welches spezifische Problem soll diese Abstraktion lösen?"

    Ein weniger erfahrener Ingenieur könnte fragen:

    "Wo sollte diese Hilfsfunktion platziert werden?"

    Ein erfahrenerer Ingenieur fragt hingegen:

    "Wer ist eigentlich für dieses Verhaltensmuster verantwortlich?"

    Ein weniger erfahrener Ingenieur könnte fragen:

    ">Wie bereiten wir uns auf die kommenden Anforderungen vor?"

    Ein erfahrenerer Ingenieur fragt hingegen:

    ">Welcher zukünftige Wandel ist wahrscheinlich genug, um die Hinzufügung dieser Komplexität jetzt zu rechtfertigen?"

    Und vielleicht die aussagekräftigste Frage überhaupt:

    ">Wie wird dieser Code aussehen, wenn die Person, die ihn geschrieben hat, weitergezogen ist?"

    Genau bei dieser Frage beginnt das langfristige Denken in der Ingenieurwissenschaft.

    Zusammenfassung: Langlebiger Code wirkt oft unscheinbar

    Code, der auch nach Jahren von Änderungen weiterhin gut funktioniert, ist selten der Code, der auf dem neuesten Framework, dem klügsten Designmuster oder der elegantesten Abstraktion basiert. Es handelt sich in der Regel um Code mit klaren Grenzen und nüchternen, vernünftigen Entscheidungen – solchen, bei denen ein anderer Ingenieur das Repository öffnen und verstehen kann, was vor sich geht, ohne den ursprünglichen Autor ausfindig machen zu müssen.

    Die zugrundeliegenden Prinzipien sind einfach:

    1. Organisieren nach Funktionalitäten und Verantwortlichkeiten. Funktionalitäten sollten leicht auffindbar sein und, wenn nötig, ebenso leicht entfernt werden können.
    2. Die Löschbarkeit vor der Maximierung der Wiederverwendung stellen. Nicht jeder Logikausschnitt verdient es, zu einer gemeinsam genutzten Abstraktion zu werden.
  • Schützen Sie Ihre Anwendung vor Abhängigkeiten von Drittanbietern. Wenden Sie Adapter an, wo ein Änderung durch den Anbieter sonst Auswirkungen auf einen großen Teil der Codebasis hätte.
  • Ziehen Sie einen expliziten Datenfluss vor. Ein wenig zusätzlicher, offensichtlicher Code kostet in der Regel weniger als verstecktes, implizites Verhalten.
  • Trennen Sie die Geschäftslogik von den Lebenszyklen des Frameworks. Die Aufgabe von React besteht darin, Inhalte anzuzeigen und zu koordinieren – nicht darin, jede Regel zu steuern, von der Ihr Geschäft abhängt.
  • Halten Sie Hooks auf ein enges Spektrum beschränkt. Vermeiden Sie, dass ein benutzerdefinierter Hook zu einer eigenen kleinen Anwendung heranwächst.
  • Documentieren Sie architektonische Entscheidungen. Speichern Sie die Begründung hinter ungewöhnlichen Wahlmöglichkeiten, nicht nur eine Beschreibung des aktuellen Zustands.
  • Begrenzen Sie den Einflussbereich von Änderungen. Idealerweise kann sich eine Funktion ändern, ohne dass Sie die Hälfte des Repositoriums anfassen müssen.
  • Verschließen Sie Komplexität nicht mit Qualität. Mehr Schichten hinzuzufügen macht eine Architektur nicht automatisch besser.
  • Die höchste Anerkennung, die eine Codebasis erhalten kann, lautet nicht:

    "Diese Architektur ist außergewöhnlich klug."

    Sondern:

    "Ich verstehe das."

    Denn nach fünf Jahren werden die ursprünglichen Entwickler wahrscheinlich bereits weitergezogen sein. Das Framework wird vermutlich anders sein, das Design hat sich geändert, das Produkt hat sich weiterentwickelt – und das Unternehmen selbst mag völlig anders aussehen als heute.

    Aber solange die Grenzen klar sind, die Logik einfach bleibt und die Begründungen für entscheidende Entscheidungen irgendwo festgehalten sind, kann sich der Code weiterentwickeln – genauso wie alles andere um ihn herum.

    Das ist das Erscheinungsbild von langlebigem Softwaredesign.

    Verwandte Artikel

  • Gewohnhafte API-Vertragsfehler, die die Zuverlässigkeit des Frontends zerstören — Lernen Sie zehn häufig auftretende Mängel im Backend-API-Design – von inkonsistenten Antwortstrukturen bis hin zu instabilen Paginierungen –, die das Vertrauen des Frontends untergraben, sowie wie man sie behebt.
  • Falle in der Backend-Architektur, die Frontend-First React-Teams behindern — Erklärt fünf häufige Mängel im Backend-Design in React-getriebenen Projekten – von Fehlern bei der API-Paradigmenverwendung bis hin zu instabilen Deployments – sowie die architektonischen Lösungen für eine produktionsreife Zuverlässigkeit.
  • Warum Frontend-Abstraktionen heimlich zu technischem Schuldenwerden führen — Erfahren Sie, warum vorzeitige Frontend-Abstraktionen versteckte Komplexität hinzufügen und wie Sie beurteilen können, ob gemeinsam genutzte Komponenten, Hooks oder Utilities tatsächlich entwickelt werden sollten.