Startseite / Artikel / Doppelung versus Kopplung: Entscheidung darüber, wann gemeinsamer Code vorhanden sein sollte

Doppelung versus Kopplung: Entscheidung darüber, wann gemeinsamer Code vorhanden sein sollte

Erfahren Sie, warum das Zusammenführen ähnlicher React-Komponenten oder NestJS-Dienste teurer sein kann als die Duplikation, und nutzen Sie drei Fragen, um zu entscheiden, wann eine Abstraktion ihren Platz verdient hat.

2806 Wörter

Die meisten Entwickler werden darin ausgebildet, wiederholten Code als Defekt zu betrachten: Zwei ähnliche Komponenten erkennen, eine gemeinsame Version extrahieren und weitermachen. Dieser Reflex ist oft richtig, doch er verdeckt eine Kostenfaktor, der erst Monate später zum Vorschein kommt, wenn der gemeinsame Code Benutzern dienen muss, deren Anforderungen sich verschoben haben. Wenn man die Entscheidung als Abwägung zwischen zwei verschiedenen Kosten – Duplikation und Kopplung – darstellt, erhält man eine kleine Anzahl von Fragen, um zu entscheiden, welche Kosten in React, React Native und Backend-Code in Kauf genommen werden sollen.

Die Kernthese ist einfach, lässt sich aber leicht falsch verstehen: Wiederholung ist keine Tugend, doch in manchen Situationen ist Duplikation günstiger als Kopplung. Im Folgenden finden Sie eine Anleitung, um solche Situationen zu erkennen.

Wie eine ordentliche gemeinsame Komponente zu einer Konfigurationssprache wird

Begonnen wird mit zwei Komponenten, die das Bild einer Person anzeigen. Eine davon ist für normale Benutzer bestimmt:

<UserAvatar user={user} />

Der andere ist für Trainer bestimmt:

<TrainerAvatar trainer={trainer} />

Am ersten Tag sind sie fast nicht voneinander zu unterscheiden. Jeder zeigt Folgendes an:

  • eine Profilbild
  • einen Ersatz, wenn das Bild fehlt
  • identische Abmessungen
  • einen Ladezustand

Die offensichtliche Schlussfolgerung ist, dass ein einziger Komponente beide Aufgaben übernehmen sollte, weshalb ein generischer Avatar angezeigt wird:

<Avatar
  imageUrl={...}
  fallback={...}
  size="medium"
/>

Es scheint ein klarer Vorteil zu sein: weniger Code, nur ein Ort zur Fehlerbehebung. Doch im Laufe der Zeit ändern sich die Anforderungen. Benutzer benötigen einen Platzhalter, der die Datenschutz-Einstellungen respektiert. Trainer brauchen ein Verifizierungsabzeichen. Benutzer können auf Initialen ausweichen. Trainer sollen anzeigen, ob sie derzeit verfügbar sind. Jeder Aufruf landet bei der gemeinsamen Komponente als weitere Eigenschaft:

<Avatar
  imageUrl={...}
  fallback={...}
  size="medium"
  showInitials={...}
  showVerification={...}
  showAvailability={...}
  privacyMode={...}
/>

Weitere Anforderungen kommen ständig hinzu, und jede davon wird zu einem weiteren Aspekt. Schließlich kennt das „allgemeine“ Avatarmodell Nutzer, Trainer, Datenschutzregeln, Verifizierungsprozesse, Verfügbarkeit sowie verschiedene Geschäftslogiken – alles Dinge, die nichts mit dem Zeichnen eines Kreises mit einem Bild darin zu tun haben. Technisch gesehen ist es zwar weiterhin wiederverwendbar, doch es ist nicht mehr einfach. Die Komplexität wurde nie beseitigt; sie wurde lediglich in eine einzige Datei zusammengefasst, wodurch jeder Nutzer nun von ihr abhängig ist. Wenn dieses Muster der exponentiellen Zunahme von Eigenschaften Ihnen bekannt vorkommt, behandelt der Artikel unter wann wiederverwendbare React-Komponenten kontraproduktiv sind die Refaktorisierung solcher Komponenten ausführlicher.

Kod teilen bedeutet eine gemeinsame Zukunft teilen

Der Punkt, der selten diskutiert wird, ist, dass das Extrahieren gemeinsam genutzten Codes nicht nur eine Entscheidung bezüglich der Implementierung ist. Es verbindet die Zukunft der Nutzer miteinander. Wenn Komponente A und Komponente B beide von derselben Abstraktion abhängen, kann jede Änderung an A nun B beeinträchtigen oder umgestalten. Wiederverwendung schafft eine Beziehung, und Beziehungen bedeuten Kopplung.

Das verändert die Frage, die gestellt werden sollte. „Können diese beiden Dinge Code teilen?“ lässt sich fast immer mit ja beantworten. Die bessere Frage lautet: Sollten diese beiden Dinge eine gemeinsame Zukunft haben?

Zwei Codeblöcke können heute textlich identisch sein und dennoch zu unzusammenhängenden Teilen des Domänenbereichs gehören. Ihre Implementierung stimmt überein; ihre Gründe für Änderungen müssen das jedoch nicht. Dieser Unterschied sagt weitaus besser über die Wartungskosten aus als eine Zählung der duplizierten Zeilen. Er steht in engem Zusammenhang mit dem Prinzip der einzigen Verantwortung, das üblicherweise als „Ein Modul sollte einen Grund für eine Änderung haben“ formuliert wird, wobei ein Grund für eine Änderung eine Gruppe von Personen bedeutet, deren Anfragen diese auslösen.

Wenn duplizierte Markup-Strukturen Unabhängigkeit schaffen

Betrachten wir eine Benutzerkarte:

<UserCard />

und eine Zahlungskarte:

<PaymentCard />

Derzeit rendern beide dieselbe Struktur:

<div className="card">
  <h3>{title}</h3>
  <p>{description}</p>
</div>

Eine gemeinsame Karte ist der natürliche nächste Schritt:

<Card />

Nehmen wir nun an, die für den Benutzer sichtbare Karte wird häufig neu gestaltet, während die Zahlungskarte durch völlig unterschiedliche Produkt- und Konformitätsanforderungen eingeschränkt ist. Die übereinstimmende Markup-Struktur ist rein zufällig; die unternehmerische Bedeutung wird nicht geteilt. Die Trennung beider Komponenten verursacht zwar einige doppelte JSX-Zeilen, dafür erhält jedes Konzept einen eigenen Raum, in dem es unabhängig entwickelt werden kann. Diese Duplikation bringt etwas Konkretes mit sich: die Möglichkeit, eine Komponente zu ändern, ohne mit den Verantwortlichen der anderen koordinieren zu müssen. Aus dieser Sicht sind zwei getrennte Komponenten keine nachlässige Architektur – sie können vielmehr eine bewusste Trennlinie darstellen.

Es gibt eine wichtige Nuance speziell für React. Eine rein visuelle Hülle, ein gestalteter Container ohne fachliche Bedeutung, kann oft sicher geteilt werden, denn der Grund für Änderungen liegt im Designsystem und nicht in einer Produktfunktion. Die Falle tritt ein, wenn der geteilte Teil anfängt, fachliche Entscheidungen aufzunehmen, die einem bestimmten Nutzer zustehen.

Fragen Sie, warum zwei Dinge gleich sind – nicht, wie man sie zusammenführt

Im Alltag neigt man dazu, die erste Frage zu stellen, wenn man Wiederholungen sieht. Zunächst ist der Instinkt: „Wie kann ich das abstrahieren?“ Eine nützlichere Abfolge lautet:

  • Warum sind diese beiden Dinge im Moment gleich?
  • Werden sie weiterhin gleich bleiben – und aus demselben Grund?

Die zweite Frage ist schwieriger, denn sie zwingt einen dazu, aufzuhören, Syntax zu vergleichen, und stattdessen die Absicht im Fokus zu haben.

Identische Implementierungen können unterschiedliche Konzepte darstellen

Nehmen wir zwei Formatierungshilfen, eine für Benutzer:

formatUserName(user)

und eine für Trainer:

formatTrainerName(trainer)

Nehmen wir an, beide enthalten derzeit denselben Inhalt:

return `${firstName} ${lastName}`;

Dann werden sie zu einer einzigen Hilfe zusammengeführt:

formatFullName(person)

Das scheint unbedenklich, bis sich die Regeln unterscheiden. Benutzernamen müssen möglicherweise berücksichtigen:

  • präferierte Namen
  • Privatsphäre-Einstellungen
  • Lokalisierungsregeln

Trainernamen möglicherweise benötigen:

  • berufliche Titel
  • Zertifizierungen
  • spezifische Richtlinien für Anzeigennamen

Die ursprünglichen Funktionen hätten ohne Probleme entlang dieser getrennten Wege weiterentwickelt werden können. Die generische Hilfe behauptet hingegen aus Namensgründen, dass Benutzer und Trainer derselben Art von „Person“ angehören, was nicht mehr zutrifft. Das ist das weniger offensichtliche Risiko der Abstraktion:

Eine Abstraktion teilt nicht nur Code; sie kodiert außerdem eine Aussage darüber, wie das Domänenmodell strukturiert ist.

Wenn diese Aussage falsch ist, führt die Abstraktion den nächsten Entwickler aktiv in die Irre, der zu Recht annimmt, dass alles, was formatFullName genannt wird, auf jede Person im System zutrifft.

Die Folgen einer vorzeitigen Abstraktion zeigen sich später

Vorzeitige Abstraktionen sind verlockend, weil sich bereits am Tag ihrer Einführung alle sichtbaren Metriken verbessern. Die Datei wird beispielsweise von etwas wie:

100 lines

auf:

60 lines

verkleinert – die Duplikate verschwinden, der Pull Request wirkt ordentlicher und die Abstraktion erscheint elegant. Die Kosten werden hinausgezögert. Ein Jahr später muss jemand das Verhalten für einen Nutzer ändern und stellt fest, dass der gemeinsam genutzte Komponente noch sieben weitere Nutzer hat. Anstatt das Risiko einzugehen, diese ebenfalls zu beeinträchtigen, erstellt man einen neuen Branch:

if (variant === "A") ...
else if (variant === "B") ...
else if (variant === "C") ...

Dann kommt ein weiterer Eigenschaftswert, eine weitere Flagge – ein Kompatibilitätsweg für ältere Bildschirme. Die Abstraktion bleibt bestehen, doch ihre konzeptionelle Einfachheit nicht. So enden Codebasen mit Komponenten, deren Schnittstelle so aussieht:

<UniversalThing
  mode="..."
  variant="..."
  type="..."
  compact
  showHeader
  showFooter
  enableSomething
  disableSomethingElse
/>

Zu diesem Zeitpunkt ist die Komponente keine Abstraktion mehr, sondern zu einer kleinen Konfigurationssprache für mehrere unzusammenhängende Anwendungsfälle geworden. Jede Kombination dieser Eigenschaftswerte stellt einen Zustand dar, über den man nachdenken muss – und die meisten Kombinationen wurden nie getestet.

Wiederverwendung kann Änderungen erschweren, nicht erleichtern

Die Ironie besteht darin, dass gemeinsam genutzter Code erstellt wird, um Änderungen günstiger zu machen – doch übermäßige Wiederverwendung macht sie oft teurer. Der Grund dafür ist der Blastradius: die Menge an Elementen, die durch eine einzige Änderung beeinflusst werden können.

Vor der Abstraktion hatte jede Funktion ihre eigene Komponente:

Feature A → Component A
Feature B → Component B

Danach leitet jede Funktion über einen gemeinsamen Bestandteil weiter:

Feature A ─┐
Feature B ─┼→ Shared Component
Feature C ─┘

Das zweite Diagramm weist weniger duplizierten Code auf, doch ein komplexeres Abhängigkeitsnetz. Daher lautet die sinnvolle Vergleichsfrage nicht „Welche Version hat weniger Duplikate?“, sondern „Welche Version macht die Kosten zukünftiger Änderungen vorhersehbarer?“ Wenn eine Änderung an Funktion B gegenüber den Funktionen A und C regresstestet werden muss, macht der gemeinsame Komponententeil die Änderungen weniger vorhersehbar – obwohl er den Code kürzer macht.

Warum React eine vorzeitige Zusammenführung fördert

React macht das Extrahieren nahezu reibungslos. Ein Button erscheint:

<Button />

und wird wiederverwendbar. Dann ein Kartenelement:

<Card />

Dann ein Modal-Fenster:

Modal />

Dann ein Formularfeld:

FormField />

Dann ein benutzerdefinierter Hook:

useSomething()

Bald gibt es eine interne Komponentenbibliothek, die in der gesamten Anwendung verwendet wird. Ein Großteil davon ist tatsächlich wertvoll. Einige Elemente sollten eindeutig geteilt werden:

  • eine Schaltfläche, die das Design-System der Anwendung konsistent darstellt
  • Niederschwelligenbarkeits-Primitiven wie Fokumanagement oder zugängliche Beschriftungen
  • Bereichskonzepte, die wirklich stabil sind

Die Wiederverwendung an sich ist kein Problem. Problematisch ist es, Dinge nur deshalb zu wiederverwenden, weil sie ähnlich aussehen. Geteilte UI-Primitiven funktionieren gut, solange sie frei von Geschäftsregeln bleiben – ein Punkt, der auch in Design von Komponenten auf der Grundlage von Verantwortlichkeiten statt Wiederverwendung erörtert wird.

React Native vervielfacht die Zustände

Mobile Apps bringen eine weitere Dimension mit sich. Zwei Bildschirme können ähnlich aussehen, während sie ganz unterschiedliche Anforderungen hinsichtlich ihres Lebenszyklus haben. Ein Komponent, der auf einem Bildschirm gut funktioniert, muss später möglicherweise mit Folgendem umgehen:

  • dass die App in den Hintergrund verschiebt wird
  • dem Verhalten der Tastatur
  • eingeschränkten Netzwerkverbindungen
  • Unterschieden zwischen den Plattformen iOS und Android
  • Rechten
  • Deep Links
  • verschiedenen Gerätedimensionen
  • Navigationszuständen
  • Offline-Modus

Falls alles von Anfang an generisch gestaltet wird, muss die gemeinsam genutzte Komponente letztendlich über jedes Umfeld Bescheid wissen, in dem sie laufen könnte. Sie wird „flexibel“, und Flexibilität hat ihren Preis: Jede neue Option vervielfacht die Anzahl der möglichen Zustände. Jeder dieser Zustände muss von jemandem letztendlich verstanden, getestet und gewartet werden. Zwei Optionen ergeben vier Kombinationen; fünf ergeben 32.

Backend-Dienste fallen in dieselbe Falle

Es handelt sich dabei nicht nur um ein Problem des Frontends. Stellen Sie sich zwei NestJS-Dienste vor:

UserService
TrainerService

Ursprünglich können sie dieselben Operationen bereitstellen:

create()
findById()
update()
delete()

Eine generische Basisklasse erscheint verlockend:

BaseService<T>

Manchmal funktioniert das und spart echten Code. Doch sobald sich die Geschäftsregeln für Benutzer und Trainer unterscheiden, beginnt die Basisklasse Ausnahmen zu sammeln. Zuerst eine Typüberprüfung:

if (entityType === "user") ...

dann ein überschreibbarer Hook vor den Aktualisierungen:

protected beforeUpdate(...)

danach ein weiterer nach der Erstellung:

protected afterCreate(...)

Und allmählich entsteht eine ganze Reihe von Erweiterungspunkten, deren einziger Zweck darin besteht, dafür zu sorgen, dass ein generischer Service je nach Domain unterschiedlich funktioniert. Dadurch wird doppelte Codebasis gegenüber bedingter Komplexität eingetauscht, die in der Regel viel schwieriger zu verstehen ist – schließlich bedeutet das Verständnis des Verhaltens einer Entität nun, die Basisklasse, ihre Hooks sowie alle Überschreibungen gemeinsam zu lesen. Wenn das bekannt vorkommt, bietet die Diskussion über die Strukturierung von NestJS-Domänen mit DDD-Regeln eine ergänzende Sicht darauf, wo Grenzen liegen sollten.

Das ist kein Argument für Kopieren und Einfügen

Zu dem Schluss zu kommen, dass Doppelungen vorteilhaft sind, wäre genauso vereinfachend. Doppelungen haben tatsächliche Kosten:

  • Falls zehn Systeme dieselbe Geschäftsregel unabhängig voneinander umsetzen, kann ein Fehlerbehebungsversuch zehn Änderungen erfordern, und das Auslassen einer solchen führt zu inkonsistentem Verhalten.
  • Falls jeweils zwanzig Komponenten dasselbe Zugänglichkeitsverhalten implementieren, wird es sehr schwierig, diese Konsistenz aufrechtzuerhalten.
  • Falls mehrere Anwendungen von derselben stabilen Schnittstelle abhängen, ist das Teilen dieser Schnittstelle äußerst wertvoll.

Der eigentliche Punkt ist enger gefasst: Duplizierung und Kopplung sind zwei verschiedene Arten von Kosten. Gute Ingenieurarbeit bedeutet, die für das Problem geeignete Kostenart auszuwählen, anstatt stets eine davon zu minimieren.

Drei Fragen vor dem Extrahieren einer Abstraktion

Wenn zwei Codeabschnitte ähnlich aussehen, halten Sie inne und beantworten Sie diese Fragen, bevor Sie sie zusammenführen.

Ändern sie aus demselben Grund?

Dies ist der wichtigste Punkt. Wenn sich die Anforderungen an ein Produkt tendenziell gleichzeitig für A und B ändern, ist eine gemeinsame Verwaltung wahrscheinlich sinnvoll. Wenn sich A aufgrund eines bestimmten Geschäftsdrucks ändert und B aus einem anderen Grund, ist die heutige identische Implementierung kein starker Beweis dafür, dass sie zusammengehören.

Bedeuten sie dasselbe?

Der Code kann identisch sein, während seine Semantik unterschiedlich ist – und gerade die Semantik entwickelt sich weiter. Ein Preis sowie ein KontoBalanz können zwar beide einfache Zahlen sein, doch das rechtfertigt es nicht, beides überall in einen einzigen Alias zusammenzufassen:

type Amount = number;

Die Darstellung stimmt überein; die Bedeutung jedoch nicht. Ein Preis kann Währungs- und Steuerregeln aufweisen, während ein Balanz Überziehungslimits enthält. Sie als unterschiedliche Konzepte zu betrachten, selbst wenn beides heute Zahlen sind, ermöglicht es, diese Unterschiede kostengünstig zu bewahren.

Beseitigt das Duplikate oder nur Zeilen?

Das sind unterschiedliche Ergebnisse. Eine gute Abstraktion beseitigt doppelte Konzepte. Eine schlechte verringert lediglich die Größe der Dateien. Dreizig Zeilen aus zwei Komponenten in eine 40-Zeilen-Hilfsfunktion mit acht Eigenschaften zu übertragen, verbessert nicht unbedingt etwas; die Komplexität ist lediglich umgezogen und erfordert nun eine zu wartende Schnittstelle.

Bewusste Duplikation ist eine legitime Wahl

Es ist sinnvoll, zwei Codeabschnitte getrennt zu lassen, wenn sie:

  • klein sind
  • einfach strukturiert sind
  • wahrscheinlich unabhängig voneinander weiterentwickelt werden
  • eine kritische Invarianz nicht schützen
  • nicht zu einem gemeinsamen Konzept im gleichen Bereich gehören

Der Grund, sie in Ruhe zu lassen, liegt nicht im Mangel an Fähigkeiten bezüglich Abstraktionen, sondern in der Erkenntnis, was eine Abstraktion kosten würde. Die Fähigkeit, Wiederholungen zu betrachten und „noch nicht“ zu entscheiden, ist ein Zeichen der Reife, nicht der Trägheit. Nicht jede Wiederholung stellt technischen Schulden dar – manchmal ist es einfach nur Wiederholung.

Wann wird Duplikation zu einem Warnsignal?

Auch die andere Seite ist genauso wichtig. Einige Formen der Duplikation sind ein klares Zeichen dafür, etwas zu konsolidieren:

  • dieselbe komplexe Geschäftsregel, die an mehreren Stellen kopiert wurde
  • drei Anwendungen, die jeweils denselben Authentifizierungsablauf umsetzen
  • mehrere Teams, die ein und denselben API-Vertrag einhalten müssen
  • eine Änderung einer Regel, bei der man sich zehn verschiedene Stellen merken muss

In solchen Fällen ist die richtige Frage: Welches Wissen wird dupliziert? Das ist weitaus wichtiger als die Anzahl der wiederholten Zeilen, denn das, was vermieden werden muss, ist die Duplizierung von Wissen – nicht von Text.

DRY dreht sich stets um Wissen

"Don't Repeat Yourself" wird oft als ">Niemals denselben Code zweimal schreiben" interpretiert. Die ursprüngliche Formulierung in The Pragmatic Programmer bezieht sich jedoch auf Wissen: Jeder Wissensgegenstand sollte in einem System eine einzige, autoritative Darstellung haben. Das sind unterschiedliche Regeln.

Zwei Komponenten können ähnlichen JSX enthalten, ohne Geschäftskenntnisse zu duplizieren. Gleichzeitig können zwei völlig unterschiedlich aussehende Funktionen jeweils dieselbe Geschäftsregel kodieren – beispielsweise eine Rabattschwelle, die sowohl in einer Bezahloberflächenkomponente als auch in einem Backend-Validierer fest codiert ist. Der zweite Fall ist der gefährliche, denn er führt stillschweigend zu Abweichungen. Statt also zu fragen, ob etwas wiederverwendet werden kann, sollte man sich fragen wo diese Kenntnisse untergebracht werden sollten.

Lassen Sie die Abstraktion ihren Platz finden

Es ist nicht notwendig, sofort eine Abstraktion zu entwerfen, sobald Duplikationen auftreten. Die Wiederholung eine Weile bestehen zu lassen und zu beobachten, wie sich die Kopien entwickeln, ist eine valide Strategie. Wenn die zweite und dritte Anwendungsfallgruppe weiterhin in dieselbe Richtung entwickeln, wird die richtige Form offensichtlich. Das gemeinsame Verhalten wird dann entdeckt, anstatt erfunden zu werden.

Dieser Unterschied ist bedeutend. Eine Abstraktion, die aus drei wirklich ähnlichen Anwendungsfallen extrahiert wird, ist in der Regel viel robuster als eine, die aus einem einzigen Anwendungsfall entwickelt wird, um zwei hypothetische zukünftige Fälle zu berücksichtigen. Das ist die Begründung für die bekannte Heuristik der „Regel der Drei“. Mit anderen Worten:

Die Abstraktion basiert darauf, was der Code nach Ihrem aktuellen Verständnis ist – nicht darauf, wie er Ihrer Vorstellung nach werden könnte.

Ziel ist ein anpassungsfähiges Design, nicht wiederverwendbarer Code

Wiederverwendung ist an sich kein nützlicher Maßstab für die Qualität einer Architektur. Bessere Indikatoren sind:

  • wie einfach der Code zu verstehen ist
  • wie isoliert eine typische Änderung erfolgt
  • wie vorhersehbar der Einflussbereich einer Änderung ist
  • ob Geschäftskonzepte klar voneinander getrennt bleiben
  • ob die Grenzen an sinnvollen Stellen liegen
  • ob das System ohne Angst weiterentwickelt werden kann
  • Mannchmal führen diese Kriterien zu einer eleganten gemeinsamen Abstraktion. Manchmal führen sie dazu, dass zwei nahezu identische Komponenten nebeneinander stehen – und das kann ein besseres Design sein, denn die beiden waren nie dasselbe. Sie sehen nur *heute* ähnlich aus.

    Haupterkenntnisse

    • Das Teilen von Code schafft eine Abhängigkeit zwischen den Nutzern; betrachten Sie jede Extraktion als Entscheidung dafür, dass ihre Zukunft miteinander verbunden ist.
    • Bewerten Sie Kandidaten für eine Abstraktion anhand ihres Änderungsgrundes und ihrer Bedeutung, nicht anhand textlicher Ähnlichkeit.
    • Prop-Flags, Varianten-Branches und überschreibbare Hooks sind Anzeichen dafür, dass ein gemeinsamer Codeabschnitt für unzusammenhängende Bereiche genutzt wird.
    • Duplizieren Sie kleine, einfache, unabhängig entwickelte Codeabschnitte frei; konsolidieren Sie hingegen duplizierte Kenntnisse wie Geschäftsregeln, Verträge und Sicherheitsabläufe.
    • Man sollte Abstraktionen bevorzugen, die aus mehreren tatsächlichen Anwendungsfällen gewonnen wurden, anstatt solcher, die für hypothetische Fälle entworfen wurden.
    • Vor dem Zusammenführen zweier ähnlicher Komponenten sollte man prüfen, ob man Wiederverwendung schafft oder eine Beziehung herstellt – und ob diese Beziehung länger bestehen sollte als die gespeicherten Codezeilen.

    Verwandte Artikel