Heim > Web-Frontend > js-Tutorial > Hauptteil

Waschen Sie Ihren Code: Teilen und erobern oder verschmelzen und entspannen

Linda Hamilton
Freigeben: 2024-11-11 19:30:03
Original
951 Leute haben es durchsucht

Washing your code: divide and conquer, or merge and relax

Sie lesen einen Auszug aus meinem Buch über sauberen Code „Washing your code“. Erhältlich als PDF, EPUB sowie als Taschenbuch- und Kindle-Ausgabe. Sichern Sie sich jetzt Ihr Exemplar.


Zu wissen, wie man Code in Module oder Funktionen organisiert und wann der richtige Zeitpunkt ist, eine Abstraktion einzuführen, anstatt Code zu duplizieren, ist eine wichtige Fähigkeit. Eine weitere Fähigkeit ist das Schreiben von generischem Code, den andere effektiv nutzen können. Es gibt ebenso viele Gründe, den Code aufzuteilen wie ihn zusammenzuhalten. In diesem Kapitel werden wir einige dieser Gründe besprechen.

Lass Abstraktionen wachsen

Wir Entwickler hassen es, die gleiche Arbeit zweimal zu erledigen. DRY ist für viele ein Mantra. Wenn wir jedoch zwei oder drei Codeteile haben, die das Gleiche tun, ist es möglicherweise noch zu früh, eine Abstraktion einzuführen, egal wie verlockend das auch sein mag.

Info: Das Don't Repeat Yourself (DRY)-Prinzip verlangt, dass „jedes Wissen eine einzige, eindeutige, maßgebliche Darstellung innerhalb eines Systems haben muss“, was oft als interpretiert wird Jegliche Code-Duplizierung ist strengstens verboten.

Lebe eine Weile mit dem Schmerz der Codeduplizierung; Vielleicht ist es am Ende gar nicht so schlimm, und der Code ist tatsächlich nicht genau derselbe. Ein gewisses Maß an Codeduplizierung ist gesund und ermöglicht es uns, Code schneller zu iterieren und weiterzuentwickeln, ohne befürchten zu müssen, dass etwas kaputt geht.

Es ist auch schwierig, eine gute API zu entwickeln, wenn wir nur ein paar Anwendungsfälle berücksichtigen.

Die Verwaltung von gemeinsam genutztem Code in großen Projekten mit vielen Entwicklern und Teams ist schwierig. Neue Anforderungen für ein Team funktionieren möglicherweise nicht für ein anderes Team und brechen deren Code, oder wir haben am Ende ein nicht zu wartendes Spaghettimonster mit Dutzenden von Bedingungen.

Stellen Sie sich vor, Team A fügt seiner Seite ein Kommentarformular hinzu: einen Namen, eine Nachricht und eine Schaltfläche zum Senden. Dann benötigt Team B ein Feedback-Formular, damit es die Komponente von Team A findet und versucht, sie wiederzuverwenden. Dann möchte Team A auch ein E-Mail-Feld, weiß aber nicht, dass Team B seine Komponente verwendet, also fügt es ein erforderliches E-Mail-Feld hinzu und unterbricht die Funktion für Team B-Benutzer. Dann benötigt Team B ein Telefonnummernfeld, weiß aber, dass Team A die Komponente ohne dieses verwendet, und fügt daher eine Option zum Anzeigen eines Telefonnummernfelds hinzu. Ein Jahr später hassen sich die beiden Teams dafür, dass sie den Code des anderen gebrochen haben, und die Komponente ist voller Bedingungen und kann nicht gewartet werden. Beide Teams würden viel Zeit sparen und gesündere Beziehungen zueinander pflegen, wenn sie separate Komponenten beibehalten würden, die aus gemeinsam genutzten Komponenten auf niedrigerer Ebene bestehen, wie z. B. einem Eingabefeld oder einer Schaltfläche.

Tipp: Es könnte eine gute Idee sein, anderen Teams die Verwendung unseres Codes zu verbieten, es sei denn, er ist als geteilt konzipiert und gekennzeichnet. Der Dependency Cruiser ist ein Tool, das bei der Einrichtung solcher Regeln helfen könnte.

Manchmal müssen wir eine Abstraktion zurücksetzen. Wenn wir anfangen, Bedingungen und Optionen hinzuzufügen, sollten wir uns fragen: Handelt es sich immer noch um eine Variation derselben Sache oder um eine neue Sache, die getrennt werden sollte? Das Hinzufügen zu vieler Bedingungen und Parameter zu einem Modul kann die Verwendung der API und die Wartung und das Testen des Codes erschweren.

Duplikation ist billiger und gesünder als die falsche Abstraktion.

Info: Eine tolle Erklärung finden Sie im Artikel „The Wrong Abstraction“ von Sandi Metz.

Je höher die Codeebene, desto länger sollten wir warten, bevor wir ihn abstrahieren. Dienstprogrammabstraktionen auf niedriger Ebene sind viel offensichtlicher und stabiler als Geschäftslogik.

Die Größe spielt nicht immer eine Rolle

Code-Wiederverwendung ist nicht der einzige oder sogar wichtigste Grund, einen Codeabschnitt in eine separate Funktion oder ein separates Modul zu extrahieren.

Codelänge wird oft als Maß dafür verwendet, wann wir ein Modul oder eine Funktion aufteilen sollten, aber die Größe allein macht den Code nicht schwer zu lesen oder zu warten.

Einen linearen Algorithmus, auch einen langen, in mehrere Funktionen aufzuteilen und diese dann nacheinander aufzurufen, führt selten dazu, dass der Code besser lesbar ist. Das Springen zwischen Funktionen (und noch mehr zwischen Dateien) ist schwieriger als das Scrollen, und wenn wir uns die Implementierung jeder Funktion ansehen müssen, um den Code zu verstehen, dann war die Abstraktion nicht die richtige.

Info:Egon Elbre hat einen schönen Artikel über die Psychologie der Codelesbarkeit geschrieben.

Hier ist ein Beispiel, adaptiert aus dem Google Testing Blog:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ich habe so viele Fragen zur API der Pizza-Klasse, aber mal sehen, welche Verbesserungen die Autoren vorschlagen:

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Was bereits komplex und kompliziert war, ist jetzt noch komplexer und komplizierter, und die Hälfte des Codes besteht nur aus Funktionsaufrufen. Das macht den Code zwar nicht leichter verständlich, macht es aber nahezu unmöglich, damit zu arbeiten. Der Artikel zeigt nicht den vollständigen Code der überarbeiteten Version, vielleicht um den Punkt überzeugender zu machen.

Pierre „catwell“ Chapuis schlägt in seinem Blogbeitrag vor, Kommentare anstelle neuer Funktionen hinzuzufügen:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Das ist schon viel besser als die Split-Version. Eine noch bessere Lösung wäre, die APIs zu verbessern und den Code klarer zu gestalten. Pierre schlägt vor, dass das Vorheizen des Ofens nicht Teil der createPizza()-Funktion sein sollte (und ich selbst viele Pizzen backe, da stimme ich voll und ganz zu!), da der Ofen im wirklichen Leben bereits vorhanden und wahrscheinlich bereits heiß von der vorherigen Pizza ist. Pierre schlägt außerdem vor, dass die Funktion die Schachtel und nicht die Pizza zurückgeben sollte, da im ursprünglichen Code die Schachtel nach all der Magie des Schneidens und Verpackens irgendwie verschwindet und wir am Ende die geschnittene Pizza in unseren Händen halten.

Es gibt viele Möglichkeiten, eine Pizza zuzubereiten, genauso wie es viele Möglichkeiten gibt, ein Problem zu codieren. Das Ergebnis sieht möglicherweise gleich aus, aber einige Lösungen sind einfacher zu verstehen, zu ändern, wiederzuverwenden und zu löschen als andere.

Die Benennung kann auch dann ein Problem sein, wenn alle extrahierten Funktionen Teile desselben Algorithmus sind. Wir müssen Namen erfinden, die klarer als der Code und kürzer als Kommentare sind – keine leichte Aufgabe.

Info: Wir sprechen über das Kommentieren von Code im Kapitel „Kommentare vermeiden“ und über die Benennung im Kapitel „Benennung ist schwierig“.

In meinem Code werden Sie wahrscheinlich nicht viele kleine Funktionen finden. Meiner Erfahrung nach sind die nützlichsten Gründe für die Aufteilung des Codes Häufigkeit ändern und Grund ändern.

Separater Code, der sich häufig ändert

Beginnen wir mit Häufigkeit ändern. Die Geschäftslogik ändert sich viel häufiger als die Dienstprogrammfunktionen. Es ist sinnvoll, Code, der sich häufig ändert, von Code zu trennen, der sehr stabil ist.

Das Kommentarformular, das wir weiter oben in diesem Kapitel besprochen haben, ist ein Beispiel für Ersteres; Ein Beispiel für Letzteres ist eine Funktion, die CamelCase-Strings in Kebab-Case konvertiert. Das Kommentarformular wird sich im Laufe der Zeit wahrscheinlich ändern und divergieren, wenn neue Geschäftsanforderungen entstehen. Es ist unwahrscheinlich, dass sich die Funktion zur Groß-/Kleinschreibung ändert, und sie kann an vielen Stellen sicher wiederverwendet werden.

Stellen Sie sich vor, wir erstellen eine hübsche Tabelle, um einige Daten anzuzeigen. Wir denken vielleicht, dass wir dieses Tabellendesign nie wieder brauchen werden, also beschließen wir, den gesamten Code für die Tabelle in einem einzigen Modul zu belassen.

Im nächsten Sprint erhalten wir die Aufgabe, der Tabelle eine weitere Spalte hinzuzufügen, also kopieren wir den Code einer vorhandenen Spalte und ändern dort ein paar Zeilen. Im nächsten Sprint müssen wir eine weitere Tabelle mit demselben Design hinzufügen. Im nächsten Sprint müssen wir das Design der Tabellen ändern…

Unser Tabellenmodul hat mindestens drei Gründe für eine Änderung, bzw. Verantwortlichkeiten:

  • Neue Geschäftsanforderungen, wie eine neue Tabellenspalte;
  • Benutzeroberflächen- oder Verhaltensänderungen, wie etwa das Hinzufügen einer Sortierung oder die Größenänderung von Spalten;
  • Designänderungen, wie das Ersetzen von Rändern durch gestreifte Reihenhintergründe.

Dadurch ist das Modul schwieriger zu verstehen und schwieriger zu ändern. Präsentationscode sorgt für viel Ausführlichkeit und erschwert das Verständnis der Geschäftslogik. Um eine Änderung an den Verantwortlichkeiten vorzunehmen, müssen wir mehr Code lesen und ändern. Dies macht es schwieriger und langsamer, beides zu iterieren.

Eine generische Tabelle als separates Modul löst dieses Problem. Um nun einer Tabelle eine weitere Spalte hinzuzufügen, müssen wir nur eines der beiden Module verstehen und ändern. Wir müssen nichts über das generische Tabellenmodul wissen, außer seiner öffentlichen API. Um das Design aller Tabellen zu ändern, müssen wir nur den Code des generischen Tabellenmoduls ändern und müssen wahrscheinlich überhaupt keine einzelnen Tabellen berühren.

Abhängig von der Komplexität des Problems ist es jedoch in Ordnung und oft auch besser, mit einem monolithischen Ansatz zu beginnen und später eine Abstraktion zu extrahieren.

Sogar die Wiederverwendung von Code kann ein triftiger Grund für die Trennung von Code sein: Wenn wir eine Komponente auf einer Seite verwenden, werden wir sie wahrscheinlich bald auf einer anderen Seite benötigen.

Halten Sie Code zusammen, der sich gleichzeitig ändert

Es könnte verlockend sein, jede Funktion in ein eigenes Modul zu extrahieren. Allerdings hat es auch Nachteile:

  • Andere Entwickler denken vielleicht, dass sie die Funktion woanders wiederverwenden können, aber in Wirklichkeit ist diese Funktion wahrscheinlich nicht generisch oder nicht ausreichend getestet, um wiederverwendet zu werden.
  • Das Erstellen, Importieren und Wechseln zwischen mehreren Dateien verursacht unnötigen Mehraufwand, wenn die Funktion nur an einer Stelle verwendet wird.
  • Solche Funktionen bleiben oft in der Codebasis, lange nachdem der Code, der sie verwendet hat, verschwunden ist.

Ich bevorzuge es, kleine Funktionen, die nur in einem Modul verwendet werden, am Anfang des Moduls beizubehalten. Auf diese Weise müssen wir sie nicht importieren, um sie im selben Modul zu verwenden, aber es wäre umständlich, sie an einem anderen Ort wiederzuverwenden.

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Im obigen Code haben wir eine Komponente (FormattedAddress) und eine Funktion (getMapLink()), die nur in diesem Modul verwendet werden, daher werden sie oben in der Datei definiert.

Wenn wir diese Funktionen testen müssen (und das sollten wir auch!), können wir sie aus dem Modul exportieren und zusammen mit der Hauptfunktion des Moduls testen.

Gleiches gilt für Funktionen, die nur zusammen mit einer bestimmten Funktion oder Komponente genutzt werden sollen. Wenn Sie sie im selben Modul belassen, wird klarer, dass alle Funktionen zusammengehören, und diese Funktionen werden leichter erkennbar.

Ein weiterer Vorteil besteht darin, dass wir beim Löschen eines Moduls automatisch seine Abhängigkeiten löschen. Code in gemeinsam genutzten Modulen bleibt oft für immer in der Codebasis, da es schwierig ist zu wissen, ob er noch verwendet wird (obwohl TypeScript dies einfacher macht).

Info: Solche Module werden manchmal als tiefe Module bezeichnet: relativ große Module, die komplexe Probleme kapseln, aber über einfache APIs verfügen. Das Gegenteil von tiefen Modulen sind flache Module: viele kleine Module, die miteinander interagieren müssen.

Wenn wir häufig mehrere Module oder Funktionen gleichzeitig ändern müssen, ist es möglicherweise besser, sie in einem einzigen Modul oder einer einzigen Funktion zusammenzuführen. Dieser Ansatz wird manchmal als Colocation bezeichnet.

Hier sind ein paar Beispiele für Colocation:

  • React-Komponenten: Behalten Sie alles, was eine Komponente benötigt, in derselben Datei, einschließlich Markup (JSX), Stile (CSS in JS) und Logik, anstatt sie jeweils in eine eigene Datei zu unterteilen, wahrscheinlich in einem separaten Ordner.
  • Tests: Speichern Sie die Tests neben der Moduldatei und nicht in einem separaten Ordner.
  • Ducks-Konvention für Redux: Behalten Sie verwandte Aktionen, Aktionsersteller und Reduzierungen in derselben Datei, anstatt sie in drei Dateien in separaten Ordnern zu speichern.

So ändert sich der Dateibaum durch Colocation:

Getrennt Koloziert
Separated Colocated
React components
src/components/Button.tsx src/components/Button.tsx
styles/Button.css
Tests
src/util/formatDate.ts src/util/formatDate.ts
tests/formatDate.ts src/util/formatDate.test.ts
Ducks
src/actions/feature.js src/ducks/feature.js
src/actionCreators/feature.js
src/reducers/feature.js
Komponenten reagieren<🎜> src/components/Button.tsx src/components/Button.tsx styles/Button.css <🎜>Tests<🎜> src/util/formatDate.ts src/util/formatDate.ts tests/formatDate.ts src/util/formatDate.test.ts <🎜>Enten<🎜> src/actions/feature.js src/ducks/feature.js src/actionCreators/feature.js src/reducers/feature.js

Info: Um mehr über Colocation zu erfahren, lesen Sie den Artikel von Kent C. Dodds.

Eine häufige Beschwerde über Colocation ist, dass die Komponenten dadurch zu groß werden. In solchen Fällen ist es besser, einige Teile zusammen mit dem Markup, den Stilen und der Logik in ihre eigenen Komponenten zu extrahieren.

Die Idee der Colocation steht auch im Widerspruch zur Trennung von Belangen – einer veralteten Idee, die Webentwickler dazu veranlasste, HTML, CSS und JavaScript in separaten Dateien (und oft in separaten Teilen des Dateibaums) zu speichern zu lang und zwingt uns, drei Dateien gleichzeitig zu bearbeiten, um selbst die grundlegendsten Änderungen an Webseiten vorzunehmen.

Info: Der Änderungsgrund ist auch als Single-Responsibility-Prinzip bekannt, das besagt, dass „jedes Modul, jede Klasse oder jede Funktion die Verantwortung für einen einzelnen Teil der Funktionalität tragen sollte.“ Die Verantwortung wird von der Software bereitgestellt und diese Verantwortung sollte vollständig von der Klasse übernommen werden.“

Fegen Sie diesen hässlichen Code unter den Teppich

Manchmal müssen wir mit einer API arbeiten, die besonders schwierig zu verwenden oder fehleranfällig ist. Es erfordert beispielsweise mehrere Schritte in einer bestimmten Reihenfolge oder den Aufruf einer Funktion mit mehreren Parametern, die immer gleich sind. Dies ist ein guter Grund, eine Hilfsfunktion zu erstellen, um sicherzustellen, dass wir es immer richtig machen. Als Bonus können wir jetzt Tests für diesen Code schreiben.

String-Manipulationen – wie URLs, Dateinamen, Groß-/Kleinschreibung oder Formatierung – sind gute Kandidaten für die Abstraktion. Höchstwahrscheinlich gibt es bereits eine Bibliothek für das, was wir tun möchten.

Betrachten Sie dieses Beispiel:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Es dauert einige Zeit, bis man erkennt, dass dieser Code die Dateierweiterung entfernt und den Basisnamen zurückgibt. Es ist nicht nur unnötig und schwer zu lesen, sondern geht auch davon aus, dass die Erweiterung immer aus drei Zeichen besteht, was möglicherweise nicht der Fall ist.

Lassen Sie es uns mit einer Bibliothek, dem integrierten Node.js-Pfadmodul, neu schreiben:

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Jetzt ist klar, was passiert, es gibt keine magischen Zahlen und es funktioniert mit Dateierweiterungen beliebiger Länge.

Andere Kandidaten für eine Abstraktion sind Datumsangaben, Gerätefunktionen, Formulare, Datenvalidierung, Internationalisierung und mehr. Ich empfehle, nach einer vorhandenen Bibliothek zu suchen, bevor Sie eine neue Dienstprogrammfunktion schreiben. Wir unterschätzen oft die Komplexität scheinbar einfacher Funktionen.

Hier sind ein paar Beispiele für solche Bibliotheken:

  • Lodash: Hilfsfunktionen aller Art.
  • Date-fns: Funktionen zum Arbeiten mit Datumsangaben, z. B. Parsen, Bearbeiten und Formatieren.
  • Zod: Schemavalidierung für TypeScript.

Segne das Inline-Refactoring!

Manchmal lassen wir uns hinreißen und erstellen Abstraktionen, die den Code weder vereinfachen noch kürzer machen:

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Noch ein Beispiel:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Das Beste, was wir in solchen Fällen tun können, ist, das allmächtige Inline-Refactoring anzuwenden: Ersetzen Sie jeden Funktionsaufruf durch seinen Hauptteil. Keine Abstraktion, kein Problem.

Das erste Beispiel wird:

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Und das zweite Beispiel wird:

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Das Ergebnis ist nicht nur kürzer und besser lesbar; Jetzt müssen die Leser nicht mehr raten, was diese Funktionen bewirken, da wir jetzt native JavaScript-Funktionen und -Features ohne hausgemachte Abstraktionen verwenden.

In vielen Fällen ist ein bisschen Wiederholung gut. Betrachten Sie dieses Beispiel:

function FormattedAddress({ address, city, country, district, zip }) {
  return [address, zip, district, city, country]
    .filter(Boolean)
    .join(', ');
}

function getMapLink({ name, address, city, country, zip }) {
  return `https://www.google.com/maps/?q=${encodeURIComponent(
    [name, address, zip, city, country].filter(Boolean).join(', ')
  )}`;
}

function ShopsPage({ url, title, shops }) {
  return (
    <PageWithTitle url={url} title={title}>
      <Stack as="ul" gap="l">
        {shops.map(shop => (
          <Stack key={shop.name} as="li" gap="m">
            <Heading level={2}>
              <Link href={shop.url}>{shop.name}</Link>
            </Heading>
            {shop.address && (
              <Text variant="small">
                <Link href={getMapLink(shop)}>
                  <FormattedAddress {...shop} />
                </Link>
              </Text>
            )}
          </Stack>
        ))}
      </Stack>
    </PageWithTitle>
  );
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Es sieht völlig in Ordnung aus und wirft bei der Codeüberprüfung keine Fragen auf. Wenn wir jedoch versuchen, diese Werte zu verwenden, zeigt die automatische Vervollständigung nur Zahlen anstelle der tatsächlichen Werte an (siehe Abbildung). Dies macht es schwieriger, den richtigen Wert auszuwählen.

Washing your code: divide and conquer, or merge and relax

Wir könnten die baseSpacing-Konstante einbinden:

const file = 'pizza.jpg';
const prefix = file.slice(0, -4);
// → 'pizza'
Nach dem Login kopieren
Nach dem Login kopieren

Jetzt haben wir weniger Code, es ist genauso einfach zu verstehen und die automatische Vervollständigung zeigt die tatsächlichen Werte an (siehe Abbildung). Und ich glaube nicht, dass sich dieser Code oft ändern wird – wahrscheinlich nie.

Washing your code: divide and conquer, or merge and relax

Trennen Sie „Was“ und „Wie“

Betrachten Sie diesen Auszug aus einer Formularvalidierungsfunktion:

const file = 'pizza.jpg';
const prefix = path.parse(file).name;
// → 'pizza'
Nach dem Login kopieren
Nach dem Login kopieren

Es ist ziemlich schwer zu verstehen, was hier vor sich geht: Die Validierungslogik ist mit Fehlermeldungen vermischt, viele Prüfungen werden wiederholt …

Wir können diese Funktion in mehrere Teile aufteilen, von denen jeder nur für eine Sache verantwortlich ist:

  • eine Liste von Validierungen für ein bestimmtes Formular;
  • eine Sammlung von Validierungsfunktionen, wie z. B. isEmail();
  • Eine Funktion, die alle Formularwerte anhand einer Liste von Validierungen validiert.

Wir können die Validierungen deklarativ als Array beschreiben:

// my_feature_util.js
const noop = () => {};

export const Utility = {
  noop
  // Many more functions…
};

// MyComponent.js
function MyComponent({ onClick }) {
  return <button onClick={onClick}>Hola!</button>;
}

MyComponent.defaultProps = {
  onClick: Utility.noop
};
Nach dem Login kopieren
Nach dem Login kopieren

Jede Validierungsfunktion und die Funktion, die Validierungen ausführt, sind ziemlich allgemein gehalten, sodass wir sie entweder abstrahieren oder eine Bibliothek eines Drittanbieters verwenden können.

Jetzt können wir für jedes Formular eine Validierung hinzufügen, indem wir beschreiben, welche Felder welche Validierungen benötigen und welcher Fehler angezeigt werden soll, wenn eine bestimmte Prüfung fehlschlägt.

Info: Den vollständigen Code und eine detailliertere Erklärung dieses Beispiels finden Sie im Kapitel „Bedingungen vermeiden“.

Ich nenne diesen Prozess Trennung von „Was“ und „Wie“:

  • Das „Was“ sind die Daten – die Liste der Validierungen für ein bestimmtes Formular;
  • Das „Wie“ sind die Algorithmen – die Validierungsfunktionen und die Validierungs-Runner-Funktion.

Die Vorteile sind:

  • Lesbarkeit: Oft können wir das „Was“ deklarativ definieren, indem wir grundlegende Datenstrukturen wie Arrays und Objekte verwenden.
  • Wartbarkeit: Wir ändern das „Was“ häufiger als das „Wie“, und jetzt sind sie getrennt. Wir können das „Was“ aus einer Datei, z. B. JSON, importieren oder aus einer Datenbank laden, wodurch Aktualisierungen ohne Codeänderungen möglich werden oder Nicht-Entwicklern die Möglichkeit gegeben werden, diese durchzuführen.
  • Wiederverwendbarkeit: Oft ist das „Wie“ allgemein gehalten und wir können es wiederverwenden oder sogar aus einer Bibliothek eines Drittanbieters importieren.
  • Testbarkeit: Jede Validierung und die Validierungsläuferfunktion sind isoliert und wir können sie separat testen.

Vermeiden Sie Monster-Dienstprogrammdateien

Viele Projekte haben eine Datei namens utils.js, helpers.js oder misc.js, in die Entwickler Hilfsfunktionen einfügen, wenn sie keinen besseren Ort dafür finden. Oft werden diese Funktionen nirgendwo anders wiederverwendet und bleiben für immer in der Dienstprogrammdatei, sodass sie ständig wächst. So entstehen Monster-Utility-Dateien.

Monster-Dienstprogrammdateien haben mehrere Probleme:

  • Schlechte Auffindbarkeit: Da sich alle Funktionen in derselben Datei befinden, können wir sie nicht mit dem Fuzzy-Dateiöffner in unserem Code-Editor finden.
  • Sie überleben möglicherweise ihre Aufrufer:Oft werden solche Funktionen nie wieder wiederverwendet und bleiben in der Codebasis, selbst nachdem der Code, der sie verwendet hat, entfernt wurde.
  • Nicht allgemein genug:Solche Funktionen sind oft für einen einzelnen Anwendungsfall konzipiert und decken andere Anwendungsfälle nicht ab.

Das sind meine Faustregeln:

  • Wenn die Funktion klein ist und nur einmal verwendet wird, behalten Sie sie im selben Modul, in dem sie verwendet wird.
  • Wenn die Funktion lang ist oder mehr als einmal verwendet wird, legen Sie sie in einer separaten Datei im Ordner „util“, „shared“ oder „helpers“ ab.
  • Wenn wir mehr Organisation wünschen, können wir, anstatt Dateien wie utils/validators.js zu erstellen, verwandte Funktionen (jede in einer eigenen Datei) in einem Ordner gruppieren: utils/validators/isEmail.js.

Vermeiden Sie Standardexporte

JavaScript-Module verfügen über zwei Arten von Exporten. Der erste ist benannte Exporte:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Was wie folgt importiert werden kann:

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Und das zweite sind Standardexporte:

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Was wie folgt importiert werden kann:

function FormattedAddress({ address, city, country, district, zip }) {
  return [address, zip, district, city, country]
    .filter(Boolean)
    .join(', ');
}

function getMapLink({ name, address, city, country, zip }) {
  return `https://www.google.com/maps/?q=${encodeURIComponent(
    [name, address, zip, city, country].filter(Boolean).join(', ')
  )}`;
}

function ShopsPage({ url, title, shops }) {
  return (
    <PageWithTitle url={url} title={title}>
      <Stack as="ul" gap="l">
        {shops.map(shop => (
          <Stack key={shop.name} as="li" gap="m">
            <Heading level={2}>
              <Link href={shop.url}>{shop.name}</Link>
            </Heading>
            {shop.address && (
              <Text variant="small">
                <Link href={getMapLink(shop)}>
                  <FormattedAddress {...shop} />
                </Link>
              </Text>
            )}
          </Stack>
        ))}
      </Stack>
    </PageWithTitle>
  );
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ich sehe keine wirklichen Vorteile von Standardexporten, aber sie haben mehrere Probleme:

  • Schlechtes Refactoring: Durch das Umbenennen eines Moduls mit einem Standardexport bleiben vorhandene Importe häufig unverändert. Dies ist bei benannten Exporten nicht der Fall, bei denen alle Importe nach dem Umbenennen einer Funktion aktualisiert werden.
  • Inkonsistenz: Standardmäßig exportierte Module können unter jedem Namen importiert werden, was die Konsistenz und Greppbarkeit der Codebasis verringert. Benannte Exporte können auch mit einem anderen Namen und dem Schlüsselwort as importiert werden, um Namenskonflikte zu vermeiden. Dies ist jedoch expliziter und erfolgt selten versehentlich.

Info:Wir sprechen mehr über Greppability im Abschnitt „Grepbaren Code schreiben“ des Kapitels Andere Techniken.

Leider erfordern einige APIs von Drittanbietern, wie z. B. React.lazy(), Standardexporte, aber in allen anderen Fällen bleibe ich bei benannten Exporten.

Vermeiden Sie Fassfeilen

Eine Barrel-Datei ist ein Modul (normalerweise mit dem Namen index.js oder index.ts), das eine Reihe anderer Module erneut exportiert:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Der Hauptvorteil sind sauberere Importe. Anstatt jedes Modul einzeln zu importieren:

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Wir können alle Komponenten aus einer Barrel-Datei importieren:

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Fassdateien weisen jedoch mehrere Probleme auf:

  • Wartungskosten:Wir müssen einen Export jeder neuen Komponente in einer Barrel-Datei hinzufügen, zusammen mit zusätzlichen Elementen wie Arten von Dienstprogrammfunktionen.
  • Leistungskosten: Das Einrichten von Tree Shaking ist komplex und Barrel-Dateien führen oft zu einer erhöhten Bundle-Größe oder Laufzeitkosten. Dies kann auch Hot-Reload, Unit-Tests und Linters verlangsamen.
  • Zirkuläre Importe: Der Import aus einer Barrel-Datei kann zu einem zirkulären Import führen, wenn beide Module aus denselben Barrel-Dateien importiert werden (z. B. importiert die Button-Komponente die Box-Komponente).
  • Entwicklererfahrung:Navigation zur Funktionsdefinition navigiert zur Barrel-Datei statt zum Quellcode der Funktion; und Autoimport kann verwirrt sein, ob aus einer Barrel-Datei statt aus einer Quelldatei importiert werden soll.

Info: TkDodo erklärt die Nachteile von Fassfeilen ausführlich.

Die Vorteile von Fassfeilen sind zu gering, um ihren Einsatz zu rechtfertigen, daher empfehle ich, sie zu meiden.

Eine Art von Barrel-Dateien, die ich besonders nicht mag, sind solche, die eine einzelne Komponente exportieren, nur um sie als ./components/button statt als ./components/button/button importieren zu können.

Bleiben Sie hydriert

Um die DRYer (Entwickler, die ihren Code nie wiederholen) zu trollen, hat jemand einen anderen Begriff geprägt: WET, alles zweimal schreiben oder wir haben Spaß am Tippen, was darauf hindeutet, dass wir den Code duplizieren sollten mindestens zweimal, bis wir es durch eine Abstraktion ersetzen. Es ist ein Witz und ich bin mit der Idee nicht ganz einverstanden (manchmal ist es in Ordnung, Code mehr als zweimal zu duplizieren), aber es ist eine gute Erinnerung daran, dass alle guten Dinge in Maßen am besten sind.

Betrachten Sie dieses Beispiel:

function createPizza(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  if (order.kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind === 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Dies ist ein extremes Beispiel für das Trocknen von Code, das den Code nicht besser lesbar oder wartbar macht, insbesondere wenn die meisten dieser Konstanten nur einmal verwendet werden. Es ist nicht hilfreich, hier Variablennamen anstelle tatsächlicher Zeichenfolgen zu sehen.

Lassen Sie uns alle diese zusätzlichen Variablen einbinden. (Leider unterstützt das Inline-Refactoring in Visual Studio Code das Inlining von Objekteigenschaften nicht, daher müssen wir es manuell durchführen.)

function prepare(order) {
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });
  addToppings(pizza, order.kind);
  return pizza;
}

function addToppings(pizza, kind) {
  if (kind === 'Veg') {
    pizza.toppings = vegToppings;
  } else if (kind === 'Meat') {
    pizza.toppings = meatToppings;
  }
}

function bake(pizza) {
  const oven = new Oven();
  heatOven(oven);
  bakePizza(pizza, oven);
}

function heatOven(oven) {
  if (oven.temp !== cookingTemp) {
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }
}

function bakePizza(pizza, oven) {
  if (!pizza.baked) {
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }
}

function pack(pizza) {
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(pizza.size);
  pizza.ready = box.close();
}

function createPizza(order) {
  const pizza = prepare(order);
  bake(pizza);
  pack(pizza);
  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Jetzt haben wir deutlich weniger Code und es ist einfacher zu verstehen, was vor sich geht, und Tests einfacher zu aktualisieren oder zu löschen.

Ich bin in Tests auf so viele hoffnungslose Abstraktionen gestoßen. Dieses Muster kommt beispielsweise sehr häufig vor:

function createPizza(order) {
  // Prepare pizza
  const pizza = new Pizza({
    base: order.size,
    sauce: order.sauce,
    cheese: 'Mozzarella'
  });

  // Add toppings
  if (order.kind == 'Veg') {
    pizza.toppings = vegToppings;
  } else if (order.kind == 'Meat') {
    pizza.toppings = meatToppings;
  }

  const oven = new Oven();

  if (oven.temp !== cookingTemp) {
    // Heat oven
    while (oven.temp < cookingTemp) {
      time.sleep(checkOvenInterval);
      oven.temp = getOvenTemp(oven);
    }
  }

  if (!pizza.baked) {
    // Bake pizza
    oven.insert(pizza);
    time.sleep(cookTime);
    oven.remove(pizza);
    pizza.baked = true;
  }

  // Box and slice
  const box = new Box();
  pizza.boxed = box.putIn(pizza);
  pizza.sliced = box.slicePizza(order.size);
  pizza.ready = box.close();

  return pizza;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Dieses Muster versucht, wiederholte mount(...)-Aufrufe in jedem Testfall zu vermeiden, macht Tests jedoch verwirrender als nötig. Lassen Sie uns die mount()-Aufrufe einbinden:

function FormattedAddress({ address, city, country, district, zip }) {
  return [address, zip, district, city, country]
    .filter(Boolean)
    .join(', ');
}

function getMapLink({ name, address, city, country, zip }) {
  return `https://www.google.com/maps/?q=${encodeURIComponent(
    [name, address, zip, city, country].filter(Boolean).join(', ')
  )}`;
}

function ShopsPage({ url, title, shops }) {
  return (
    <PageWithTitle url={url} title={title}>
      <Stack as="ul" gap="l">
        {shops.map(shop => (
          <Stack key={shop.name} as="li" gap="m">
            <Heading level={2}>
              <Link href={shop.url}>{shop.name}</Link>
            </Heading>
            {shop.address && (
              <Text variant="small">
                <Link href={getMapLink(shop)}>
                  <FormattedAddress {...shop} />
                </Link>
              </Text>
            )}
          </Stack>
        ))}
      </Stack>
    </PageWithTitle>
  );
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Außerdem funktioniert das beforeEach-Muster nur, wenn wir jeden Testfall mit denselben Werten initialisieren möchten, was selten der Fall ist:

const file = 'pizza.jpg';
const prefix = file.slice(0, -4);
// → 'pizza'
Nach dem Login kopieren
Nach dem Login kopieren

Um einige Duplikate beim Testen von React-Komponenten zu vermeiden, füge ich oft ein defaultProps-Objekt hinzu und verteile es in jedem Testfall:

const file = 'pizza.jpg';
const prefix = path.parse(file).name;
// → 'pizza'
Nach dem Login kopieren
Nach dem Login kopieren

Auf diese Weise gibt es nicht zu viele Duplikate, aber gleichzeitig ist jeder Testfall isoliert und lesbar. Der Unterschied zwischen Testfällen ist jetzt klarer, da die einzigartigen Eigenschaften jedes Testfalls einfacher zu erkennen sind.

Hier ist eine extremere Variante desselben Problems:

// my_feature_util.js
const noop = () => {};

export const Utility = {
  noop
  // Many more functions…
};

// MyComponent.js
function MyComponent({ onClick }) {
  return <button onClick={onClick}>Hola!</button>;
}

MyComponent.defaultProps = {
  onClick: Utility.noop
};
Nach dem Login kopieren
Nach dem Login kopieren

Wir können die beforeEach()-Funktion auf die gleiche Weise einbinden wie im vorherigen Beispiel:

const findByReference = (wrapper, reference) =>
  wrapper.find(reference);

const favoriteTaco = findByReference(
  ['Al pastor', 'Cochinita pibil', 'Barbacoa'],
  x => x === 'Cochinita pibil'
);

// → 'Cochinita pibil'
Nach dem Login kopieren

Ich würde sogar noch weiter gehen und die Methode test.each() verwenden, weil wir denselben Test mit einer Reihe unterschiedlicher Eingaben ausführen:

function MyComponent({ onClick }) {
  return <button onClick={onClick}>Hola!</button>;
}

MyComponent.defaultProps = {
  onClick: () => {}
};
Nach dem Login kopieren

Jetzt haben wir alle Testeingaben mit ihren erwarteten Ergebnissen an einem Ort gesammelt, was das Hinzufügen neuer Testfälle erleichtert.

Info: Schauen Sie sich meine Jest- und Vitest-Spickzettel an.


Die größte Herausforderung bei Abstraktionen besteht darin, ein Gleichgewicht zwischen zu starr und zu flexibel zu finden und zu wissen, wann man mit der Abstraktion beginnen und wann man aufhören sollte. Es lohnt sich oft abzuwarten, ob wir etwas wirklich abstrahieren müssen – oft ist es besser, es nicht zu tun.

Es ist schön, eine globale Schaltflächenkomponente zu haben, aber wenn sie zu flexibel ist und über ein Dutzend boolescher Requisiten verfügt, um zwischen verschiedenen Variationen zu wechseln, wird sie schwierig zu verwenden sein. Wenn es jedoch zu starr ist, erstellen Entwickler ihre eigenen Schaltflächenkomponenten, anstatt eine gemeinsame zu verwenden.

Wir sollten darauf achten, dass andere unseren Code wiederverwenden. Allzu oft führt dies zu einer engen Kopplung zwischen Teilen der Codebasis, die unabhängig sein sollten, was die Entwicklung verlangsamt und zu Fehlern führt.

Denken Sie darüber nach:

  • Koordinieren von zugehörigem Code in derselben Datei oder demselben Ordner, um das Ändern, Verschieben oder Löschen zu erleichtern.
  • Bevor Sie einer Abstraktion eine weitere Option hinzufügen, überlegen Sie, ob dieser neue Anwendungsfall wirklich dorthin gehört.
  • Bevor Sie mehrere Codeteile zusammenführen, die ähnlich aussehen, überlegen Sie, ob sie tatsächlich dieselben Probleme lösen oder ob sie zufällig gleich aussehen.
  • Bevor Sie Tests DRY machen, überlegen Sie, ob sie dadurch besser lesbar und wartbar sind, oder ob ein bisschen Codeduplizierung kein Problem darstellt.

Wenn Sie Feedback haben, senden Sie mir eine Nachricht, twittern Sie mich, eröffnen Sie ein Problem auf GitHub oder senden Sie mir eine E-Mail an artem@sapegin.ru. Holen Sie sich Ihr Exemplar.

Das obige ist der detaillierte Inhalt vonWaschen Sie Ihren Code: Teilen und erobern oder verschmelzen und entspannen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage