Zehn alltägliche JavaScript-Muster und die damit verbundenen Kompromisse
Schutzklauseln, Abfragekarten, Ergebnisobjekte, AbortController, allSettled, Fabriken und kleine Caches: Wann sich jedes Muster lohnt und wann es nach hinten losgeht.
Quellen: I Reuse These 10 JavaScript Patterns in Almost Every Project
Egal ob es sich um eine React-Dashboard-Anwendung, eine Node.js-API oder ein kleines Automatisierungs-Skript handelt – letztendlich stößt man auf dieselben Probleme: Funktionen erhalten unvollständige Daten, asynchrone Antworten kommen aus dem Nichts, bedingte Anweisungen wachsen aufgrund sich ändernder Geschäftsregeln an, Kopien derselben Transformationen weichen voneinander ab und die Fehlerbehandlung verschwimmt die Grenze zwischen „das ist fehlgeschlagen“ und „es gab einfach nichts zu zurückgeben“. Frameworks kommen und gehen, doch diese Probleme bleiben erstaunlich konstant. Die zehn unten aufgeführten Muster sind bewusst unspektakuläre Lösungen dafür. Jedes davon reduziert die Anzahl der Zustände, über die eine Funktion nachdenken muss, oder macht die Verantwortlichkeiten klarer; zudem erfahren Sie für jedes Muster, ab wann es nicht mehr hilft – schließlich ersetzt keines von ihnen das eigene Urteilsvermögen.
1. Schutzklauseln halten den Hauptweg lesbar
Wenn die eigentliche Arbeit einer Funktion in mehreren verschachtelten if-Blöcken stattfindet, muss der Leser eine Reihe offener Fragen im Kopf behalten: Ist die Eingabe vorhanden? Ist der Aufrufer berechtigt? Wurde das Datensatz gefunden? Befindet er sich in einem Zustand, der diese Änderung zulässt? Jede zusätzliche Bedingung führt zu einem weiteren Einrückungsgrad und etwas, das man im Hinterkopf behalten muss.
Eine Schutzklausel beantwortet jede dieser Fragen im Voraus und bricht den Ablauf ab, sobald die Antwort „nein“ lautet. Das untenstehende Beispiel lehnt zunächst einen fehlenden ID ab, anschließend einen nicht autorisierten Benutzer, dann einen fehlenden Artikel sowie einen Artikel, der kein Entwurf ist, und erst nach allen vier Überprüfungen führt es die Aktualisierung durch.
async function publishArticle(articleId, currentUser) {
if (!articleId) {
throw new Error("Article ID is required");
}
if (!currentUser?.permissions.includes("article:publish")) {
throw new Error("You are not allowed to publish articles");
}
const article = await articleRepository.findById(articleId);
if (!article) {
throw new Error("Article not found");
}
if (article.status !== "draft") {
throw new Error(`Cannot publish an article in ${article.status} status`);
}
return articleRepository.update(articleId, {
status: "published",
});
}
Der eigentliche Vorteil liegt nicht in weniger Einrückungen, sondern in wachsender Sicherheit. Hinter dem ersten Schutzmechanismus existiert die ID bereits. Hinter dem zweiten hat der Aufrufer Berechtigungen. Hinter dem dritten ist article ein echtes Objekt, sodass das optionale Chaining nicht weiter durch den Rest des Codes ausgedehnt werden muss. Der finale update-Aufruf lässt sich ohne ungelöste Probleme lesen. Ein kleiner Randfall, der beachtet werden sollte: currentUser?.permissions.includes(...) wirft weiterhin einen TypeError aus, wenn ein Benutzerobjekt ohne Array permissions übergeben wird – genau diese Inkonsistenz behebt das nächste Muster.
Guard-Bedingungen werden zum Problem, wenn eine lange Funktion mit Ausgängen für jede kleine Bedingung übersät ist. Sie eignen sich am besten für Voraussetzungen, ungültige Zustände, fehlende Ressourcen sowie bereits abgeschlossene Ergebnisse. Wenn die Guard-Bedingungen mehrere unterschiedliche Geschäftsregeln beschreiben, trifft die Funktion vermutlich zu viele Entscheidungen und sollte in kleinere Teile aufgeteilt werden.
2. Normalisieren Sie die Daten einmal, an der Grenze
JavaScript ermöglicht es, dass Werte mit derselben Bedeutung in vielen Formen auftreten können. Ein Flag kann true, "true", 1 oder "1" sein. Eine ID kann in der Datenbank eine Zahl und in der URL ein String sein. Ein Datum kann als Date, ISO-String, Lokalstring oder Zeitstempel dargestellt werden. Wenn jede Funktion diese Werte eigenständig interpretiert, sammelt sich im Laufe der Zeit allmählich eine Vielzahl widersprüchlicher Definitionen dafür an, wie gültige Daten aussehen sollten.
Die Lösung besteht darin, die Werte einmal umzuwandeln – genau an dem Punkt, an dem sie eine bedeutende Grenze überschreiten – noch bevor der Rest des Codes darauf zugreift:
- Ein API-Handler wandelt Strings auf Transportebene in die Typen um, die die Service-Schicht erwartet.
- Ein Datenadapter überträgt eine externe Antwort in die interne Struktur des Projekts.
- Ein Formular-Übermittlungs-Handler kürzt Strings und bearbeitet optionale Felder, bevor die Geschäftslogik ausgeführt wird.
Die Zeitpunktion ist wichtig. Wenn die Umwandlung aufgeschoben wird, breiten sich schützende Überprüfungen überall aus: Eine Funktion prüft status === true, eine andere prüft status === „true“, wieder eine andere nutzt die Eigenschaft der Wahrheit. Jede Funktion funktioniert für ihren jeweiligen Aufrufer, doch die Bedeutung von status ist nicht mehr konstant.
Ein eng gefasster interner Vertrag sorgt außerdem dafür, dass die Tests gezielter sind. Wenn die Anfrageschicht bereits einen echten Booleschen Wert überlässt, benötigt der Service keine Tests für jede mögliche textuelle Schreibweise desselben. Die Normalisierungsschicht benötigt zwar weiterhin eigene Tests, doch alles dahinter wird einfacher.
Normalisierung bedeutet nicht, alles zu akzeptieren. Sie ist sinnvoll, wenn mehrere Darstellungsformen absichtlich unterstützt werden; unklare oder ungültige Werte sollten dennoch abgelehnt werden. "abc" in NaN umzuwandeln und es weiterzuleiten, ist keine Normalisierung – es handelt sich dabei um eine Verzögerung der Validierung auf einen späteren, verwirrenderen Zeitpunkt.
3. Option-Objekte machen Aufrufe selbstbeschreibend und erweiterbarer
Positionale Parameter eignen sich für ein oder zwei offensichtliche Eingabewerte. Sie werden anfällig, wenn die Funktion komplexer wird, insbesondere dann, wenn mehrere Parameter denselben Typ haben. Ein Aufruf wie createUser(name, email, true, false, 3) zwingt den Benutzer dazu, zur Definition zu gehen, um herauszufinden, was jeder Parameter bedeutet, und die Typangaben in TypeScript helfen am Aufrufort nicht weiter.
Sobald eine Funktion mehrere wertebasierte Eingaben akzeptiert, ist in der Regel ein einzelnes Options-Objekt klarer:
await createUser({
name,
email,
sendWelcomeEmail: true,
requireVerification: false,
retryLimit: 3,
});
Der Aufruf dokumentiert sich nun selbst, optionale Eigenschaften können Standardwerte haben (meist durch Destructuring in der Signatur), und das Hinzufügen einer neuen Option zwingt nicht jeden Aufrufer, seine Argumente umzuordnen. Dienstfunktionen, Konfigurationshilfen, Datenlader sowie weiterentwickelte Utility-Funktionen profitieren am meisten davon.
Dadurch verschwindet auch der typische „boolean-parameter“-Geruch. Ein einfaches true in einem Aufruf sagt fast nie an, welches Verhalten aktiviert wird; eine benannte Eigenschaft hingegen schon.
Wenden Sie es nicht überall an. add({ a, b }) ist nicht klarer als add(a, b). Verwenden Sie ein Options-Objekt, wenn die Argumente eine Konfiguration beschreiben, wenn mehrere Argumente optional sind, wenn mehrere Werte denselben Typ haben oder wenn die Signatur voraussichtlich ständig ändern wird.
4. Abfragekarten verwandeln Branching in explizite Beziehungen
Ketten von if-Anweisungen oder lange switch-Blöcke treten oft auf, wenn ein Wert einen Handler, Formatierer, Validator oder eine Strategie auswählt. Jeder neue Fall verlängert den Kontrollfluss und fügt einen weiteren Punkt hinzu, an dem das Standardverhalten berücksichtigt werden muss.
Eine Abgleichstabelle gibt die Beziehung direkt an: Dieser Schlüssel entspricht diesem Verhalten. Hier wählt der Status einer Bestellung ihren Handler aus, und ein unbekannter Status führt zu einem deutlichen Fehler statt zu einer stillen Weiterverarbeitung.
const statusHandlers = {
pending: handlePendingOrder,
paid: handlePaidOrder,
cancelled: handleCancelledOrder,
};
function processOrder(order) {
const handler = statusHandlers[order.status];
if (!handler) {
throw new Error(`Unsupported order status: ${order.status}`);
}
return handler(order);
}
Das Muster kommt besonders zur Geltung, wenn die Zweige unabhängig sind und denselben Vertrag teilen: Serialisierer und Formater, Ereignis- und Zustandshandler, Validatoren, Zugriffsrichtlinien. Die Unterstützung eines neuen Falls bedeutet das Hinzufügen einer einzigen Eintragung anstelle der Erweiterung einer Bedingung.
Bei einfachen Objekten gibt es eine subtile Falle. Schlüssel, die von Object.prototype geerbt wurden, wie beispielsweise "toString" oder "constructor", sind ebenfalls Funktionen, sodass ein unerwarteter Statuswert stattdessen eine dieser Funktionen aufrufen könnte, anstatt die Schutzmaßnahme zu aktivieren. Wenn die Schlüssel aus Benutzereingaben stammen, sollten Sie einen Map, ein mit Object.create(null) erstelltes Objekt oder eine Überprüfung mit Object.hasOwn verwenden.
Vermeiden Sie Suchtabellen, wenn die Logik tatsächlich sequenziell ist. Wenn eine Entscheidung von Kombinationen des Zustands, früheren Auswahlmöglichkeiten oder geordneten Übergängen abhängt, führt die Umwandlung in eine Tabelle in der Regel dazu, dass der Ablauf verdeckt statt klarer wird. Eine Tabelle eignet sich nur dann, wenn ein Schlüssel ein vollständiges, eigenständig verständliches Verhalten auswählt.
Halten Sie umfangreiche Logik außerhalb der Karte selbst. Benannte Handler-Funktionen erzeugen klarere Stack-Traces, lassen sich leichter isoliert testen und sorgen dafür, dass die Karte sich auf die Auswahl konzentriert.
5. Ergebnisobjekte trennen erwartete Fehler von fehlgeschlagener Ausführung
Exceptions sind das richtige Werkzeug für unerwartete Situationen, doch viele fehlgeschlagene Ergebnisse sind völlig normal. Eine Suche kann nichts finden. Eine Registrierung kann abgelehnt werden, weil die E-Mail bereits in Gebrauch ist. Ein Zustandswechsel kann durch aktuelle Regeln verboten werden. Wenn all diese Fälle als generische Exceptions übertragen werden, müssen die Aufrufer Fehlermeldungen analysieren oder breite Fehlerarten abfangen, nur um herauszufinden, was passiert ist.
Wenn eine Operation mehrere mögliche Ergebnisse hat, macht das Zurückgeben eines kleinen Ergebnisobjekts diese zu Bestandteilen des Vertrags. Die untenstehende Rabattfunktion gibt „nicht gefunden“ und „abgelaufen“ als Daten zurück, jeweils mit einem stabilen, maschinenlesbaren Code, und gibt bei Erfolg die angepasste Bestellung aus.
function applyDiscount(order, discount) {
if (!discount) {
return {
ok: false,
code: "DISCOUNT_NOT_FOUND",
message: "The requested discount does not exist.",
};
}
if (discount.expiresAt < new Date()) {
return {
ok: false,
code: "DISCOUNT_EXPIRED",
message: "The discount has expired.",
};
}
return {
ok: true,
value: {
...order,
total: order.total - discount.amount,
},
};
}
Die Aufrufer können nun je nach Wert von ok und code weiterverarbeiten, ohne menschenlesbaren Text lesen zu müssen. Die genauen Feldnamen sind weniger wichtig als die konsequente Verwendung derselben Struktur: Einige Projekte modellieren dies als unterschiedene Typen in TypeScript basierend auf ok, andere verwenden success, data und error. Entscheidend ist, dass das Ergebnis eindeutig ist. In einer echten Implementierung müsste man außerdem entscheiden, was passiert, wenn der Rabatt die Gesamtsumme der Bestellung übersteigt, da die hier gezeigte Subtraktion zu einem negativen Betrag führen kann.
Result-Objekte ersetzen nicht überall Ausnahmen. Werfen Sie eine Ausnahme, wenn eine Abhängigkeit unerwartet versagt, wenn eine Invarianz verletzt wird oder wenn die aktuelle Schicht nichts Nützliches gegen das Problem unternehmen kann. Geben Sie Ergebnisse zurück, wenn Fehler ein normaler Bestandteil des Anwendungsbereichs sind und vom Aufrufer darauf reagiert werden soll.
Ein weiterer Vorteil ist die Testbarkeit: Die Fehlerfälle sind im Rückgabetyp festgelegt, sodass sie leicht überprüft werden können. Zudem führt ein umfassendes try/catch-Konstrukt nicht mehr dazu, dass eine abgelehnte Geschäftsregel mit einem unterbrochenen Datenbankverbindung zusammengefasst wird.
6. AbortController macht die asynchrone Verantwortung explizit
Viele Fehler im Frontend entstehen durch überschneidende asynchrone Abläufe. Ein Benutzer ändert einen Suchbegriff, es wird eine zweite Anfrage gestartet, und die erste löst sich erst zuletzt auf. Jede Anfrage war erfolgreich, doch das Display zeigt veraltete Ergebnisse.
Die Anfrage ist nicht das Problem – das Eigentumsverhältnis schon. Die Anwendung muss entscheiden, welches laufende Ergebnis noch den Zustand aktualisieren darf. AbortController drückt diese Entscheidung aus:
- In einem Suchfeld: Die vorherige Anfrage abbrechen, sobald sich die Abfrage ändert.
- In einer Komponente: Die ausstehenden Aufgaben abbrechen, wenn die Komponente deinstalliert wird.
- In einem Workflow mit abhängigen Schritten: Ein gemeinsames
signalan alle Schritte weiterleiten, sodass ein einziger Abbruch die gesamte Kette stoppt.
Behandeln Sie Stornierungen absichtlich. Ein Abbruch bedeutet in der Regel, dass die Anwendung entschieden hat, dass das Ergebnis nicht mehr relevant ist – was sich von einem Fehler unterscheidet. Mit fetch wird ein abgebrochener Aufruf mit einem Fehler abgelehnt, dessen name "AbortError" lautet, sodass Sie ihn explizit erkennen können. Das Protokollieren jedes Abbruchs als Fehler überflutet Ihre Überwachung mit unnötigem Datenrauschen, während echte Netzwerkfehler als Abbrüche behandelt werden, wodurch nützliche Beweise verloren gehen.
Je stärker sich eine App auf Hintergrundabrufe, Live-Suche, Routenwechsel und aufeinanderfolgende Anfragen verlässt, desto wichtiger wird dies. Das Starten asynchroner Aufgaben in JavaScript ist unkompliziert; AbortController bestimmt, wann diese Aufgaben nicht mehr dem Aufrufer gehören. Für eine genauere Betrachtung des Suchfalls erfahren Sie, wie man Rennbedingungen behebt, die durch Debounce-Funktionen nicht gelöst werden können.
7. Promise.allSettled bewahrt teilweise Ergebnisse unabhängiger Aufgaben
Promise.all eignet sich dann, wenn das kombinierte Ergebnis sinnlos ist, es sei denn, alle Operationen sind erfolgreich. Bei unabhängigen Aufgaben ist es jedoch zu aggressiv: Ein einziger Fehler führt dazu, dass alle tatsächlich eingegangenen Werte verworfen werden.
Betrachten Sie ein Dashboard, das aus Verkaufszahlen, Benachrichtigungen, neu registrierten Nutzern sowie Informationen zum Systemzustand zusammengesetzt ist – wobei jede dieser Informationen von einer anderen Quelle stammt. Wenn ein optionales Service ausfällt, führt dies oft dazu, dass das gesamte Dashboard unbrauchbar wird, was eine schlechtere Erfahrung darstellt als die Anzeige dessen, was geladen wurde, sowie das Hervorheben dessen, was nicht funktioniert hat.
Promise.allSettled wartet auf jede Promise und berichtet über das Ergebnis jeder einzelnen als Objekt mit status: „fulfilled“ und einem value, oder mit status: „rejected“ und einem reason. Der Aufrufer kann die erfolgreichen Werte nutzen, die Fehlschläge auflisten und entscheiden, ob die gesamte Aktion noch sinnvoll ist.
Ziehen Sie nicht einfach darauf zurück, nur um zu vermeiden, dass eine Aufgabe scheitert. Einige Gruppen von Promises stellen aus geschäftlicher Sicht eine einzige Transaktion dar. Wenn ein Auftrag erstellt, Lagerbestände reserviert und eine Karte abgebucht werden müssen, müssen alle Schritte entweder erfolgreich oder gemeinsam fehlschlagen – das Sammeln der Ergebnisse führt sonst zu einem inkonsistenten Zustand; es sind kompensierende Aktionen oder eine stärkere Orchestrierungsstrategie erforderlich.
Verwenden Sie allSettled, wenn die Aufgaben tatsächlich unabhängig sind und der Aufrufer einen definierten Plan für teilweises Versagen hat. Die eigentliche Frage ist nicht, welcher Promise-Hilfsfunktion es am bequemsten ist, sondern ob nach einem Versagen eines Elements noch eine ehrliche Antwort möglich ist. Weitere Optionen werden in neun Promise-Mustern für zuverlässiges asynchrones JavaScript in der Produktion behandelt.
8. Reine Transformationsfunktionen erleichtern das Testen von Datenvorschriften
Vieler Anwendungscode mischt die Formatierung von Daten mit dem Abrufen, Protokollieren, Darstellen, Speichern sowie dem Aktualisieren des Zustands. Das Testen einer einfachen Regel erfordert daher den Neuaufbau der gesamten Umgebung darum herum.
Durch das Hinzufügen stabiler Transformationen in kleine reine Funktionen wird dieses Problem gelöst. Eine reine Funktion nimmt Eingaben entgegen, gibt Ausgaben zurück und ändert nichts außerhalb ihrer selbst. Typische Beispiele sind die Normalisierung von API-Daten, die Berechnung abgeleiteter Werte, das Gruppieren von Elementen, das Erstellen von Abfrageobjekten oder die Umwandlung von Domänendaten in ein View-Modell.
Der größere Vorteil ist eine klare Trennung zwischen dem Abrufen von Daten und ihrer Interpretation. Eine Funktion, die Mitarbeiterdaten gruppiert, muss nicht wissen, wie diese geladen wurden. Eine Funktion, die eine Anfrage sendet, benötigt den Zustand von Komponenten nicht, wenn die Werte als Argumente übergeben werden können.
Auch das Debuggen wird einfacher. Wenn korrekte Eingaben zu falschen Ausgaben führen, kann man die Transformation selbst ausprobieren. Wenn die Transformation korrekt ist, konzentriert sich die Untersuchung auf das, was die Eingabe erzeugt hat oder die Ausgabe verbraucht hat.
Nicht jede Expression verdient eine eigene Funktion; eine Ansammlung von einzeiligen Wrapper-Funktionen erschwert die Navigation, ohne zusätzlichen Sinn zu schaffen. Extrahiere Funktionen, wenn die Transformation eine wiederverwendete Geschäftsregel kodiert, auf verschiedene Weise verzweigt ist oder eigene Tests benötigt.
9. Fabriken machen Abhängigkeiten und gemeinsame Konfiguration sichtbar
Eine Fabrik ist eine der einfachsten Methoden, um zu steuern, worauf sich ein Modul verlässt. Anstatt einen Datenbankklienten, Logger, Cache oder Konfiguration aus einer globalen Stelle zu importieren, erhält die Fabrik diese als Argumente und gibt Funktionen zurück, die auf ihnen aufbauen.
Dies ist besonders nützlich in Backend-Diensten und gemeinsamen Modulen. Abhängigkeiten werden sichtbar, versteckte Kopplungen nehmen ab, und Tests können ein falsches Repository oder einen Logger verwenden, ohne die Modul-Importe zu mocken, während in der Produktion die echten verwendet werden. Die Geschäftslogik konzentriert sich weiterhin auf das Verhalten und nicht auf die Konstruktion ihrer Abhängigkeiten.
Fabriken eignen sich auch für Gruppen von Funktionen, die eine stabile Konfiguration teilen. Eine HTTP-Client-Fabrik kann einmal eine Basis-URL, ein Authentifizierungs-Token, einen Timeout sowie einen Logger erfassen und Methoden bereitstellen, die alle diese Elemente nutzen; dabei übernehmen Closures die Konfiguration, sodass sie nicht bei jedem Aufruf weitergegeben werden muss.
Es besteht die Gefahr, ein eigenes Framework für Dependency Injection zu entwickeln, wenn die Codebasis bereits eines bereitstellt oder eine einfache Import-Methode ausreicht. Verwenden Sie Factories, wenn Abhängigkeiten das Verhalten erheblich verändern, wenn Tests Kontrolle über sie benötigen oder wenn mehrere unterschiedlich konfigurierte Instanzen koexistieren müssen. Das Ziel ist es, Abhängigkeiten sichtbar zu machen – nicht, ein weiteres Framework zum Warten zu schaffen.
10. Narrow Map und WeakMap-Caches vermeiden wiederholte Arbeit
Map und WeakMap eignen sich als lokale Caches, wenn dieselben aufwändigen Berechnungen oder objektspezifische Metadaten sonst erneut durchgeführt werden müssten. Ein Parser kann Ergebnisse pro Eingabestring speichern. Ein Formatter kann teure, abgeleitete Daten wiederverwenden. Ein WeakMap, dessen Schlüssel Objekte sein müssen, kann Metadaten an diese Objekte hängen, ohne sie am Leben zu erhalten, sobald keine anderen Referenzen mehr darauf verweisen.
Das Schreiben in den Cache ist der einfachere Teil. Zu ermitteln, ob ein im Cache gespeicherter Wert noch gültig ist, ist der schwierige Teil.
Caches funktionieren auf vorhersehbare Weise falsch:
- Der Schlüssel enthält keinen Eingabewert, der das Ergebnis beeinflusst, sodass ein im Test gut funktionierender Cache in der Produktion veraltete Werte liefert.
- Die zugrunde liegenden Daten ändern sich, ohne dass etwas den Eintrag für ungültig erklärt.
- Das Wachstum ist unbegrenzt, wie bei einem modulweiten
Map, der während der gesamten Laufzeit eines Serverprozesses besteht.
Vor dem Hinzufügen eines Caches sollte man einen tatsächlich wiederkehrenden Aufwand identifizieren und dessen Verantwortlichen, Schlüsselstruktur, Lebensdauer sowie Regeln zur Ungültigkeitsklärung definieren. Oft ist eine erneute Berechnung sicherer und schnell genug. Caching lohnt sich, wenn die Arbeit aufwändig ist, die Eingabedaten stabil sind und der Anwendungsbereich eng gehalten werden kann.
Dieses Muster veranschaulicht die gesamte Liste gut. Map macht das Caching sehr einfach zu implementieren, doch einfach bedeutet nicht automatisch richtig: Die Geschwindigkeitssteigerung ist nur dann wirklich vorhanden, wenn man erklären kann, ab wann ein Eintrag nicht mehr zutrifft.
Muster sollten Entscheidungen beseitigen, nicht verbergen
Insgesamt sichern diese Muster ihren Platz dadurch, dass sie Unklarheiten beseitigen, anstatt Sprachmerkmale hervorzuheben. Wächter entfernen ungültige Pfade frühzeitig; die Normalisierung an der Peripherie liefert internem Code vorhersehbare Werte; Option-Objekte halten Aufrufe lesbar und erweiterbar; Abfragekarten zeigen direkte Beziehungen auf; Ergebnisobjekte sorgen dafür, dass Ausgaben sinnvoll bleiben; AbortController weist die Verantwortung für asynchrone Aufgaben zu; allSettled meldet teilweise fehlgeschlagene Operationen ehrlich; reine Funktionen isolieren Entscheidungen von Nebeneffekten; Fabriken machen Abhängigkeiten sichtbar; und kleine Caches vermeiden wiederholte Arbeit, wenn ihre Gültigkeit klar ist.
Jedes davon kann auch falsch angewendet werden. Das Einpacken eines zweiparametrigen Aufrufs in ein Options-Objekt ist reine Formalität; das Zurückgeben von Ergebnissen aus Code, der eigentlich Fehler werfen sollte, verschleiert die Situation; ein Map kann geordnete Logik verbergen; ein Cache kann veralten; und eine Factory kann eine Abstraktion sein, die niemand braucht. Ein Muster wird schädlich, wenn es aus Gründen der Optik gewählt wird anstelle aufgrund des tatsächlichen Drucks, den es lindert.
Kernpunkte
- Bewerten Sie ein Muster anhand des Kompromisses, den es darstellt, und nicht danach, wie raffiniert es aussieht.
- Ziehen Sie Strukturen vor, deren Kosten sichtbar sind und deren Vorteile auch nach späteren Änderungen bestehen bleiben.
- Gute Muster machen die Annahmen, Verantwortlichkeiten, Fehlermöglichkeiten sowie gültigen Abläufe einer Funktion explizit.
- Die besten Muster treten in den Hintergrund: Die Leser bemerken, dass das Problem einfacher wird, nicht das Muster selbst.