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.
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.
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; }
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; }
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; }
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.
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:
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.
Es könnte verlockend sein, jede Funktion in ein eigenes Modul zu extrahieren. Allerdings hat es auch Nachteile:
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; }
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:
So ändert sich der Dateibaum durch Colocation:
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 |
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.“
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; }
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; }
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:
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; }
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; }
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; }
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; }
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> ); }
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.
Wir könnten die baseSpacing-Konstante einbinden:
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
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.
Betrachten Sie diesen Auszug aus einer Formularvalidierungsfunktion:
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
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:
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 };
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“:
Die Vorteile sind:
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:
Das sind meine Faustregeln:
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; }
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; }
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; }
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> ); }
Ich sehe keine wirklichen Vorteile von Standardexporten, aber sie haben mehrere Probleme:
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.
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; }
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; }
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; }
Fassdateien weisen jedoch mehrere Probleme auf:
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.
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; }
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; }
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; }
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> ); }
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'
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'
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 };
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'
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: () => {} };
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:
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!