Clean Code von Robert Martin. Auszug. Wie schreibe ich klaren und schönen Code?

Ich beschloss, ein Kompendium des Buches zu schreiben, das jeder kennt, und der Autor nennt es "School of Pure Code Teachers". Martins Blick scheint zu sagen:

„Ich sehe durch dich hindurch. Folgen Sie nicht wieder den Prinzipien des sauberen Codes? “

Bild

Kapitel 1. Code bereinigen


Was ist der sauberste Martin-Versionscode in wenigen Worten? Dies ist Code ohne Duplizierung, mit einer minimalen Anzahl von Entitäten, leicht zu lesen, einfach. Als Motto könnte man wählen: "Klarheit ist vor allem!".

Kapitel 2. Bedeutungsvolle Namen


Namen müssen die Absichten des Programmierers vermitteln


Der Name einer Variablen, Funktion oder Klasse sollte angeben, warum diese Variable existiert, was sie tut und wie sie verwendet wird. Wenn der Name zusätzliche Kommentare erfordert, vermittelt er nicht die Absichten des Programmierers. Es ist besser zu schreiben, was genau gemessen wird und in welchen Einheiten.

Ein Beispiel für einen guten Variablennamen: daysSinceCreation;
Zweck: Nicht-Offensichtlichkeit entfernen.

Vermeiden Sie Fehlinformationen


Verwenden Sie keine Wörter mit versteckten Bedeutungen, die nicht beabsichtigt sind. Vorsicht vor subtilen Namensunterschieden. Zum Beispiel XYZControllerForEfficientHandlingOfStrings und XYZControllerForEfficientStorageOfStrings.

Wirklich erschreckende Beispiele für falsch informierte Namen finden sich, wenn in Variablennamen Kleinbuchstaben „L“ und Großbuchstaben „O“ verwendet werden, insbesondere in Kombinationen. Natürlich ergeben sich Probleme aufgrund der Tatsache, dass sich diese Buchstaben von den Konstanten "1" bzw. "0" fast nicht unterscheiden.

Nutze sinnvolle Unterschiede


Wenn die Namen unterschiedlich sind, sollten sie unterschiedliche Konzepte anzeigen.

"Zahlenreihen" der Form (a1, a2, ... aN) sind das Gegenteil von bewusster Namensgebung. Sie enthalten keine Informationen und geben keine Vorstellung von den Absichten des Autors.

Nichtinformative Wörter sind überflüssig. Das Wort Variable sollte niemals in Variablennamen vorkommen. Die Worttabelle sollte niemals in Tabellennamen erscheinen. Warum ist NameString besser als Name? Kann ein Name beispielsweise eine reelle Zahl sein?

Verwenden Sie Namen mit Schreibweisen: generationTimestamp ist viel besser als genymdhms.

Wählen Sie suchbare Namen


Einbuchstabige Namen können nur für lokale Variablen in kurzen Methoden verwendet werden.

Vermeiden Sie Namenskodierungsschemata


Typischerweise sind codierte Namen schlecht ausgesprochen und es ist leicht, einen Tippfehler in ihnen zu machen.

Schnittstellen und Implementierungen


Ich (der Autor des Buches) bevorzuge es, Schnittstellennamen ohne Präfixe zu belassen. Das im alten Code übliche Präfix I lenkt bestenfalls ab und vermittelt im schlimmsten Fall unnötige Informationen. Ich werde meinen Benutzern nicht mitteilen, dass sie mit einer Benutzeroberfläche zu tun haben.

Klassennamen


Klassen- und Objektnamen müssen Substantive und ihre Kombinationen sein: Customer, WikiPage, Account und AddressParser. Vermeiden Sie die Verwendung von Wörtern wie Manager, Prozessor, Daten oder Informationen in Klassennamen. Der Klassenname sollte kein Verb sein.

Methodennamen


Methodennamen sind Verben oder verbale Phrasen: postPayment, deletePage, save usw. Lese- / Schreibmethoden und Prädikate werden aus dem Wert und dem Präfix get, set gebildet und entsprechen dem javabean-Standard.

Vermeiden Sie Wortspiele


Aufgabe des Autors ist es, seinen Code so klar wie möglich zu gestalten. Der Code sollte auf einen Blick erkennbar sein, ohne dass ein sorgfältiges Studium erforderlich ist. Konzentrieren Sie sich auf das Modell der Populärliteratur, in dem der Autor seine Gedanken frei ausdrücken muss.

Fügen Sie einen aussagekräftigen Kontext hinzu.


Kontext kann mit den Präfixen addrFirstName, addrLastName, addrState usw. hinzugefügt werden. Zumindest der Codeleser wird verstehen, dass die Variablen Teil einer größeren Struktur sind. Natürlich wäre es richtiger, eine Klasse namens Address zu erstellen, damit selbst der Compiler weiß, dass die Variablen Teil von etwas anderem sind.

Variablen mit unklarem Kontext:

private void printGuessStatistics(char candidate, int count) { String number; String verb; String pluralModifier; if (count == 0) { number = "no"; verb = "are"; pluralModifier = "s"; } else if (count == 1) { number = ~_~quot quot~_~; verb = "is"; pluralModifier = ""; } else { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } String guessMessage = String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); print(guessMessage); } 

Die Funktion ist etwas lang und die Variablen werden durchgehend verwendet. Um die Funktion in kleinere semantische Fragmente zu unterteilen, sollten Sie die GuessStatisticsMessage-Klasse erstellen und drei Variablen zu Feldern dieser Klasse machen. Auf diese Weise stellen wir einen offensichtlichen Kontext für die drei Variablen bereit - jetzt ist es absolut offensichtlich, dass diese Variablen Teil von GuessStatisticsMessage sind.

Variablen mit Kontext:

 public class GuessStatisticsMessage { private String number; private String verb; private String pluralModifier; public String make(char candidate, int count) { createPluralDependentMessageParts(count); return String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); } private void createPluralDependentMessageParts(int count) { if (count == 0) { thereAreNoLetters(); } else if (count == 1) { thereIsOneLetter(); } else { thereAreManyLetters(count); } } private void thereAreManyLetters(int count) { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } private void thereIsOneLetter() { number = ~_~quot quot~_~; verb = "is"; pluralModifier = ""; } private void thereAreNoLetters() { number = "no"; verb = "are"; pluralModifier = "s"; } } 

Fügen Sie keinen redundanten Kontext hinzu


Kurznamen sind in der Regel besser als Langnamen, wenn dem Codeleser nur deren Bedeutung klar ist. Nimm nicht mehr Kontext als nötig in den Namen auf.

Kapitel 3. Funktionen


Kompakt!


Erste Regel: Funktionen sollen kompakt sein.
Die zweite Regel: Funktionen sollen noch kompakter werden.

Meine praktische Erfahrung hat mich (auf Kosten vieler Versuche und Irrtümer) gelehrt, dass Funktionen sehr klein sein sollten. Es ist wünschenswert, dass die Länge der Funktion 20 Zeilen nicht überschreitet.

Regel einer Operation


Eine Funktion darf nur eine Operation ausführen. Sie muss es gut machen. Und sonst sollte sie nichts tun. Wenn eine Funktion nur die Aktionen ausführt, die sich unter dem deklarierten Namen der Funktion auf derselben Ebene befinden, führt diese Funktion eine Operation aus.

Funktionsbereiche


Eine Funktion, die nur eine Operation ausführt, kann nicht sinnvoll in Abschnitte unterteilt werden.

Eine Abstraktionsebene pro Funktion


Um sicherzustellen, dass die Funktion „nur eine Operation“ ausführt, muss sichergestellt werden, dass sich alle Befehle der Funktion auf derselben Abstraktionsebene befinden.

Das Mischen von Abstraktionsebenen innerhalb einer Funktion führt immer zu Verwirrung.

Code von oben nach unten lesen: Downgrade-Regel


Der Code sollte sich wie eine Geschichte anhören - von oben nach unten.

Auf jede Funktion sollten Funktionen der nächsten Abstraktionsebene folgen. Auf diese Weise können Sie den Code lesen und die Abstraktionsebenen nacheinander durchlaufen, während Sie die Liste der Funktionen lesen. Ich nenne diesen Ansatz die "Herabstufungsregel".

Befehle wechseln


Das Schreiben eines kompakten Schaltbefehls ist ziemlich schwierig. Sogar ein Schaltbefehl mit nur zwei Bedingungen nimmt mehr Platz ein, als ein einzelner Block oder eine einzelne Funktion aus meiner Sicht einnehmen sollte. Es ist auch schwierig, einen Schaltbefehl zu erstellen, der eines bewirkt - Schaltbefehle führen von Natur aus immer N Operationen aus. Leider können wir nicht immer auf switch-Befehle verzichten, aber wir können zumindest sicherstellen, dass diese Befehle in einer untergeordneten Klasse ausgeblendet und nicht im Code dupliziert werden. Und natürlich kann uns der Polymorphismus dabei helfen.

Das Beispiel zeigt nur einen Vorgang, abhängig vom Mitarbeitertyp.

 public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } } 

Diese Funktion hat mehrere Nachteile. Erstens ist es großartig, und mit der Hinzufügung neuer Arten von Arbeitnehmern wird es wachsen. Zweitens führt es am offensichtlichsten mehr als eine Operation aus. Drittens verstößt es gegen das Prinzip der einheitlichen Verantwortung, da es mehrere mögliche Gründe für die Änderung gibt.

Viertens verstößt es gegen das Open-Closed-Prinzip, da der Funktionscode jedes Mal geändert werden muss, wenn neue Typen hinzugefügt werden.

Der vielleicht schwerwiegendste Nachteil ist jedoch, dass das Programm eine unbegrenzte Anzahl anderer Funktionen mit einer ähnlichen Struktur enthalten kann, zum Beispiel:

isPayday (Mitarbeiter e, Datum Datum)

oder

deliverPay (Mitarbeiter e, Geldbezahlung)

usw.

Alle diese Funktionen haben die gleiche fehlerhafte Struktur. Die Lösung für dieses Problem besteht darin, den Schaltbefehl im Fundament der abstrakten Fabrik zu vergraben und ihn niemandem zu zeigen. Die Factory verwendet den Befehl switch, um die entsprechenden Instanzen der Nachkommen von Employee zu erstellen, und ruft die Funktionen calculatorPay, isPayDay, deliverPay usw. auf, um die polymorphe Übertragung über die Employee-Schnittstelle durchzuführen.

 public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } } 

Meine allgemeine Regel für Schaltbefehle lautet, dass diese Befehle gültig sind, wenn sie einmal im Programm vorkommen, zum Erstellen polymorpher Objekte verwendet werden und sich hinter Vererbungsbeziehungen verstecken, um für den Rest des Systems unsichtbar zu bleiben. Natürlich gibt es keine Regeln ohne Ausnahmen und in einigen Situationen ist es notwendig, eine oder mehrere Bedingungen dieser Regel zu verletzen.

Verwenden Sie aussagekräftige Namen


Die Hälfte des Aufwandes zur Implementierung dieses Prinzips besteht in der Auswahl guter Namen für kompakte Funktionen, die eine einzige Operation ausführen. Je kleiner und spezialisierter die Funktion ist, desto einfacher ist es, einen aussagekräftigen Namen dafür zu wählen.

Haben Sie keine Angst, lange Namen zu verwenden. Ein langer, aussagekräftiger Name ist besser als ein kurzer, dunkler. Wählen Sie ein Schema, mit dem sich die Wörter im Funktionsnamen leicht lesen lassen, und benennen Sie sie dann so, dass sie den Zweck der Funktion beschreiben.

Funktionsargumente


Im Idealfall ist die Anzahl der Funktionsargumente Null. Das Folgende sind Funktionen mit einem Argument (unär) und mit zwei Argumenten (binär). Funktionen mit drei Argumenten (ternär) sollten nach Möglichkeit vermieden werden.

Die Ausgabeargumente verwirren die Situation noch schneller als die Eingabe. In der Regel erwartet niemand, dass eine Funktion Informationen in Argumenten zurückgibt. Wenn Sie nicht ohne Argumente auskommen, beschränken Sie sich mindestens auf ein Eingabeargument.

Konvertierungen, die ein Ausgabeargument anstelle eines Rückgabewerts verwenden, verwirren den Leser. Wenn die Funktion ihr Eingabeargument konvertiert, dann das Ergebnis
muss im Rückgabewert übergeben werden.

Flags Argumente


Das Argument Argumente sind hässlich. Die logische Bedeutung einer Funktion weiterzugeben, ist eine schreckliche Gewohnheit. Die Signatur der Methode wird sofort verkompliziert und lautstark verkündet, dass die Funktion mehr als eine Operation ausführt. Wenn das Flag wahr ist, wird eine Operation ausgeführt, und wenn falsch, wird eine andere ausgeführt.

Binäre Funktionen


Eine Funktion mit zwei Argumenten ist schwieriger zu verstehen als eine unäre Funktion. Natürlich ist in einigen Situationen eine Form mit zwei Argumenten angemessen. Zum Beispiel: Punkt p = neuer Punkt (0,0); absolut vernünftig. In unserem Fall sind jedoch zwei Argumente geordnete Komponenten mit demselben Wert.

Objekte als Argumente


Wenn eine Funktion mehr als zwei oder drei Argumente erhalten soll, ist es sehr wahrscheinlich, dass einige dieser Argumente in eine separate Klasse gepackt werden. Betrachten Sie die folgenden zwei Deklarationen:

 Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius); 

Wenn die Variablen zusammen übertragen werden (wie die Variablen x und y in diesem Beispiel), bilden sie höchstwahrscheinlich zusammen ein Konzept, das seinen eigenen Namen verdient.

Verben und Schlüsselwörter


Die Auswahl eines guten Namens für eine Funktion kann weitgehend die Bedeutung der Funktion sowie die Reihenfolge und Bedeutung ihrer Argumente erklären. In unären Funktionen müssen die Funktion selbst und ihr Argument ein natürliches Verb / Nomen-Paar bilden. Beispielsweise sieht ein Aufruf der Form write (name) sehr informativ aus.

Der Leser versteht, dass, egal wie der "Name" ist, er irgendwo "geschrieben" ist. Noch besser ist der writeField (name) -Datensatz, der angibt, dass der "Name" in das "Feld" einer Struktur geschrieben wurde.

Der letzte Eintrag ist ein Beispiel für die Verwendung von Schlüsselwörtern in einem Funktionsnamen. In dieser Form werden Argumentnamen im Funktionsnamen codiert. Beispielsweise kann assertEquals als assertExpectedEqualsActual (erwartet, tatsächlich) geschrieben werden. Dies löst weitgehend das Problem, sich an die Reihenfolge der Argumente zu erinnern.

Trennung von Befehlen und Anforderungen


Eine Funktion muss etwas tun oder eine Frage beantworten, aber nicht gleichzeitig. Entweder ändert die Funktion den Status des Objekts oder gibt Informationen zu diesem Objekt zurück. Das Kombinieren von zwei Operationen führt häufig zu Verwirrung.

Isolieren Sie Try / Catch-Blöcke


Die Try / Catch-Blöcke sehen ziemlich hässlich aus. Sie verwechseln die Struktur des Codes und die Fehlerbehandlung mit der normalen Verarbeitung. Aus diesem Grund wird empfohlen, die Körper von try- und catch-Blöcken in separaten Funktionen zuzuordnen.

Fehlerbehandlung als eine Operation


Funktionen müssen eine Operation ausführen. Fehlerbehandlung ist eine Operation. Dies bedeutet, dass die Funktion, die Fehler verarbeitet, nichts anderes tun sollte. Daraus folgt, dass, wenn das Schlüsselwort try in der Funktion vorhanden ist, es das erste Wort in der Funktion sein muss und nach den catch / finally-Blöcken nichts anderes stehen sollte.

Damit ist Kapitel 3 abgeschlossen.

Source: https://habr.com/ru/post/de485118/


All Articles