Namen im Code
Kurz gesagt: Gute Namen im Code sagen, was etwas ist und warum es existiert: Konstanten statt magischer Zahlen, boolesche Werte als Fragen und die Wörter des Geschäfts statt Abkürzungen.
Problem
Die Funktion, die niemand anfassen will
Im Code eines Online-Shops gibt es eine Funktion, durch die jede Bestellung geht:
static decimal Calc(decimal a, int t, bool f){ decimal d = 4.99m; if (a >= 50) { d = 0; } if (t == 2) { d += 7; } if (f) { d = 0; } return a + d;}Console.WriteLine($"{Calc(40, 1, false):F2} €");Console.WriteLine($"{Calc(40, 2, false):F2} €");Console.WriteLine($"{Calc(60, 2, false):F2} €");Console.WriteLine($"{Calc(40, 2, true):F2} €");44.99 €51.99 €67.00 €40.00 €Sie funktioniert. Jetzt sag, was sie tut. Was ist t? Warum addiert t == 2 7? Was ist f, und warum setzt es alles zurück? Warum genau 50?
Der Autor wusste es. Er ist vor einem Jahr gegangen. Das Marketing will kostenlose Lieferung ab 60 €, nicht ab 50 €. Dein Kollege sucht im Projekt nach 50 und findet 14 Stellen: An einer ist 50 die Grenze für die Lieferung, an einer anderen das größte Paketgewicht in Kilogramm, an einer dritten die Anzahl der Produkte pro Seite. Welche soll sich ändern?
Die Logik hier ist richtig. Das Problem ist, dass der Code dem Leser nicht sagt, was der Autor gedacht hat.
Die Idee
Der Name sagt, was und warum, nicht wie
Der Computer liest Calc(a, t, f) ohne Probleme. Ein Mensch nicht. Und Code wird viel öfter gelesen als geschrieben: bei jeder Korrektur, bei jedem Review, jedes Mal, wenn jemand einen Fehler sucht.
Ein guter Name beantwortet die Fragen, die du sonst dem Autor stellen würdest: Was ist das, warum ist es hier, wie wird es benutzt. Das nennt man einen aussagekräftigen Namen (intention-revealing name). Hier ist dieselbe Funktion, nur mit geänderten Namen:
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon){ decimal deliveryFee = 4.99m; if (subtotal >= 50) { deliveryFee = 0; } if (deliveryType == 2) { deliveryFee += 7; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}Console.WriteLine($"{TotalWithDelivery(40, 1, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, 2, false):F2} €");Console.WriteLine($"{TotalWithDelivery(60, 2, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, 2, true):F2} €");44.99 €51.99 €67.00 €40.00 €Die Ausgabe ist bis auf die letzte Ziffer dieselbe. Die Funktion liest sich jetzt wie eine Regel: Die Lieferung kostet 4,99 €, ab 50 € ist sie kostenlos, die Expresslieferung kostet 7 € mehr, und der Gutschein macht sie kostenlos.
Namen, die nicht helfen:
- ein Name, der sagt, wie, nicht was:
decimalValue,list1,tmp; - ein allgemeiner Name, der zu allem passt:
data,info,value,process,handle,manager; - eine Abkürzung, die nur der Autor kennt:
Calc,cnt,dlvFee.
Die Länge richtet sich nach dem Gültigkeitsbereich. i in einer Schleife mit drei Zeilen ist in Ordnung. Der Parameter einer Funktion, die von zehn Stellen aufgerufen wird, verdient einen vollen Namen.
Genauer, für Neugierige
Und ein Kommentar über der Funktion, t: 1 = standard, 2 = express? Er hilft, bis jemand die Zahlen ändert und den Kommentar vergisst. Ein Name kann nicht getrennt vom Code veralten, weil er Teil des Codes ist. Wann ein Kommentar hilft und wann er lügt, ist das Thema der dritten Lektion dieses Moduls.
Die Idee
Magische Zahl → benannte Konstante
Die Namen der Variablen sind jetzt an ihrem Platz, aber 4.99, 50, 7 und 2 stehen noch im Code. Eine Zahl ohne Namen im Code heißt magische Zahl: Was sie bedeutet, weiß nur der Autor, und wenn sie sich ändern muss, suchst du sie überall.
Die Lösung ist eine benannte Konstante. Du schreibst den Wert einmal an einer Stelle und benutzt überall sonst den Namen:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;const: Der Wert ist zur Kompilierzeit bekannt und kann sich nicht ändern. Der Compiler verhindert jeden Versuch. In C# schreibt man Konstanten in PascalCase (FreeDeliveryFrom), wie Methoden und Properties, nicht in Großbuchstaben.Das Marketing will 60 €: Du änderst eine Zeile, und jede Stelle, die fragt „ab wann ist die Lieferung kostenlos“, sieht den neuen Wert. Und die 50er, die Gewichte sind, bleiben unangetastet, weil sie nicht FreeDeliveryFrom sind.
Genauer, für Neugierige
Nicht jede Zahl ist magisch. 0 für „keine Gebühr“, 1 in i + 1 und 100 in einem Prozentwert versteht man aus ihrer Stelle. Eine Zahl ist magisch, wenn sie eine Geschäftsregel trägt (eine Grenze, eine Gebühr, ein Limit, ein Statuscode) oder sich ändern kann. Eine Konstante, die nach ihrem Wert benannt ist (FIFTY = 50), hilft nicht: Der Name sagt die Rolle, nicht die Zahl.
const gilt nur für Werte, die zur Kompilierzeit bekannt sind: Zahlen, Strings, bool. Für ein Objekt schreibst du static readonly: Es wird einmal zugewiesen und ändert sich danach nicht. Noch ein Unterschied: Der Wert einer const wird in den Code kopiert, der sie benutzt. Wenn ein anderes Projekt deine Konstante benutzt, sieht es den neuen Wert erst, wenn es neu kompiliert wird.Sieh es dir an
Dieselbe Rechnung, durch die Namen gelesen
Die Namen und die Konstanten zusammen:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon){ decimal deliveryFee = StandardDeliveryFee; if (subtotal >= FreeDeliveryFrom) { deliveryFee = 0; } if (deliveryType == Express) { deliveryFee += ExpressSurcharge; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}Console.WriteLine($"{TotalWithDelivery(40, Standard, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, Express, false):F2} €");Console.WriteLine($"{TotalWithDelivery(60, Express, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, Express, true):F2} €");44.99 €51.99 €67.00 €40.00 €Sieh zu, wie jede Zahl und jeder Buchstabe aus der ersten Version im Lauf der Rechnung eine Bedeutung bekommt:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon){ decimal deliveryFee = StandardDeliveryFee; if (subtotal >= FreeDeliveryFrom) { deliveryFee = 0; } if (deliveryType == Express) { deliveryFee += ExpressSurcharge; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}
Console.WriteLine($"{TotalWithDelivery(40, Express, false):F2} €");Schritt für Schritt, in Worten
- Der alte Aufruf war Calc(40, 2, false). Jetzt sagt er, was er berechnet, und 2 ist Express.
- Die Parameter: a, t, f werden zu subtotal, deliveryType, hasFreeDeliveryCoupon. Die Werte sind dieselben.
- „d = 4.99m“ wird zu „deliveryFee = StandardDeliveryFee“. Dieselben 4.99, aber jetzt weißt du, was es ist: die Gebühr für die Standardlieferung.
- 50 ist FreeDeliveryFrom: die Grenze für kostenlose Lieferung. 40 liegt darunter, also ist die Lieferung nicht kostenlos.
- „t == 2“ war ein Rätsel. deliveryType == Express ist ein Satz: Die Expresslieferung kostet ExpressSurcharge mehr.
- „if (f)“ sagte dir nichts. hasFreeDeliveryCoupon ist eine Frage, und die Antwort ist false: kein Gutschein.
- Die Bestellung plus die Lieferung. Das Programm gibt 51.99 € aus.
- Hin und zurück: dieselben Werte, dieselbe Rechnung und dieselbe Ausgabe wie bei Calc(40, 2, false). Nur die Namen haben sich geändert.
Alle drei Versionen geben dasselbe aus. Der Computer sieht keinen Unterschied. Der Unterschied ist für den Menschen, der morgen die Regel ändert. Eine Stelle sagt noch immer nichts: false im Aufruf. Darum kümmern wir uns in der nächsten Lektion.
Probier es aus
Sag die Ausgabe voraus
Eine Bestellung über genau 50 € mit Standardlieferung und eine Bestellung über 49,90 € mit Expresslieferung. Lies die Regel durch die Namen. Was gibt das Programm aus?
Console.WriteLine($"{TotalWithDelivery(50, Standard, false):F2} €");Console.WriteLine($"{TotalWithDelivery(49.90m, Express, false):F2} €");Die erste: subtotal >= FreeDeliveryFrom ist bei genau 50 wahr, deshalb ist die Lieferung 0 und die Summe 50,00 €. Die zweite liegt unter der Grenze: 4,99 € Lieferung und 7 € für den Express, also 49,90 + 11,99 = 61,89 €.
Probier es aus
Fülle die Lücke
Die Grenze für die kostenlose Lieferung ist jetzt eine Konstante. Was gehört in die Lücke?
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon)
{
decimal deliveryFee = StandardDeliveryFee;
if (subtotal >= )
{
deliveryFee = 0;
}
if (deliveryType == Express)
{
deliveryFee += ExpressSurcharge;
}
if (hasFreeDeliveryCoupon)
{
deliveryFee = 0;
}
return subtotal + deliveryFee;
}Der Vergleich benutzt den Namen der Konstante, nicht die Zahl. Die Grenze steht an einer Stelle, und die Zeile liest sich wie die Regel: „wenn die Summe mindestens die Grenze für kostenlose Lieferung erreicht“.
Die Idee
Ein boolescher Wert ist eine Frage
Ein boolescher Wert antwortet mit Ja oder Nein, deshalb ist sein Name eine Frage: isPaid, hasCoupon, canShip. Dann liest sich das if wie ein Satz:
decimal subtotal = 62.40m;bool isPaid = true;bool qualifiesForFreeDelivery = subtotal >= FreeDeliveryFrom;
if (isPaid && qualifiesForFreeDelivery){ Console.WriteLine("Wir versenden ohne Liefergebühr.");}Wir versenden ohne Liefergebühr.Was nicht funktioniert:
flag,status,check,ok: Sie sagen nicht, was die Frage ist;- eine Verneinung im Namen:
isNotPaid. Dann istif (!isNotPaid)eine doppelte Verneinung, und jeder stolpert darüber. SchreibisPaid, und setz die Verneinung in die Prüfung, wenn du sie brauchst.
Dasselbe gilt für die anderen Namen. Eine Klasse und eine Variable sind Substantive (Order, deliveryFee). Eine Funktion tut etwas, also ist sie ein Verb (SendInvoice). Eine Funktion, die wie eine Frage heißt (IsInStock), verspricht, dass sie nur fragt. In der nächsten Lektion siehst du, was passiert, wenn sie dieses Versprechen bricht.
Die Idee
Die Wörter des Shops im Code
Die besten Namen erfindest du nicht. Es sind die Wörter der Menschen, für die das Programm gemacht ist: Paket, Kurier, Nachnahme, Rückerstattung, Rechnung. Wenn im Code parcel, courier, refund steht, sprechen der Programmierer und der Mensch im Lager über dasselbe. Das sind Domänennamen (domain names).
- Ein Wort für ein Konzept. Wenn der Kunde
customerheißt, wird er in der nächsten Datei nicht zuclientund in der Datenbank zubuyer. Wenn Lesenfind…heißt, wird es anderswo nicht zuget…undfetch…. Ein anderes Wort lässt den Leser nach einer anderen Sache suchen. - Ein Wort bedeutet nicht zwei Dinge. Wenn
totaldie Summe mit Lieferung ist, hat die Summe ohne Lieferung einen eigenen Namen,subtotal. - Technische Wörter sind für technische Dinge.
queue,cache,parsersind in Ordnung, wenn genau das gemeint ist.Manager,ProcessorundHelperbedeuten meistens „Ich weiß nicht genau, was es tut“.
Probier es aus
Finde den Fehler
Die Buchhaltung beschwert sich: Eine Rechnung über 50 € ohne Mehrwertsteuer kommt auf 110 €. Die Mehrwertsteuer beträgt 20%, also müssten es 60 € sein.
static decimal Vat(decimal price){ return price * 1.20m;}
static void PrintInvoice(decimal net){ decimal total = net + Vat(net); Console.WriteLine($"Netto: {net:F2} €"); Console.WriteLine($"Gesamt mit MwSt.: {total:F2} €");}PrintInvoice(50);Netto: 50.00 €Gesamt mit MwSt.: 110.00 €In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Der Fehler ist der Name. Vat verspricht die Steuer, gibt aber den Preis mit Steuer zurück. Der Autor von PrintInvoice hat dem Namen vertraut und „die Steuer“ zum Preis addiert, also wurde der Preis doppelt gezählt. Jede Funktion für sich berechnet genau das, was in ihr steht. Nur der Name lügt. Die Lösung ist eine Funktion, deren Name und Rumpf dasselbe sagen:
const decimal VatRate = 0.20m;
static decimal VatAmount(decimal price){ return price * VatRate;}Damit beträgt die Rechnung 60,00 €.
Die Idee
Wie deine Sprache Namen schreibt
Ein guter Name in einer Sprache sieht in einer anderen merkwürdig aus. Jede Sprache hat ihre eigene Konvention, und Code, der ihr folgt, liest sich schneller, weil die Form des Namens dir sagt, was er ist.
- Klassen, Methoden, Properties und Konstanten:
PascalCase(Order,TotalWithDelivery,FreeDeliveryFrom); - lokale Variablen und Parameter:
camelCase(deliveryFee,subtotal); - private Felder:
_camelCase(_stock); - Interfaces: ein
Idavor (IPaymentMethod); - Den Wert eines Objekts liest man über eine Property:
order.Subtotal, nicht über eine MethodeGetSubtotal(). Eine Property ist ein Substantiv, eine Methode ein Verb.
Genauer, für Neugierige
Derselbe Name in den vier Sprachen:
| Java | C# | Python | JavaScript | |
|---|---|---|---|---|
| Funktion | totalWithDelivery |
TotalWithDelivery |
total_with_delivery |
totalWithDelivery |
| Variable | deliveryFee |
deliveryFee |
delivery_fee |
deliveryFee |
| Konstante | FREE_DELIVERY_FROM |
FreeDeliveryFrom |
FREE_DELIVERY_FROM |
FREE_DELIVERY_FROM |
| Klasse | Order |
Order |
Order |
Order |
| boolescher Wert | isPaid |
isPaid (Property: IsPaid) |
is_paid |
isPaid |
Die Idee
Rename: das billigste Refactoring
Einen Namen zu ändern, ohne das Verhalten zu ändern, heißt Rename. Es ist das kleinste und billigste Refactoring: Du änderst keinen Wert und keinen Vergleich, und das Programm gibt dasselbe aus.
Mach es nicht von Hand mit Suchen und Ersetzen. Der Editor weiß, welches d deine Variable ist und welches der Buchstabe in "delivery": In IntelliJ IDEA und PyCharm ist es Umschalt+F6, in VS Code F2, in Visual Studio Strg+R, Strg+R. Er ändert jede Stelle auf einmal.
Wie du in kleinen Schritten refaktorierst, sodass das Programm nach jedem funktioniert, ist das Thema der sechsten Lektion dieses Moduls. Rename ist der erste Schritt.
Übung
Der Kurier
Löse die Aufgabe in deinem Editor.
Der Kurier. Diese Funktion berechnet den Preis eines Pakets:
static decimal F(decimal w, int z){ decimal p = 3.50m; if (w > 2) { p += (w - 2) * 0.80m; } if (z == 3) { p *= 1.5m; } return p;}Console.WriteLine($"{F(1.5m, 1):F2} €");Console.WriteLine($"{F(5, 1):F2} €");Console.WriteLine($"{F(5, 3):F2} €");3.50 €5.90 €8.85 €Die Regel lautet: Ein Paket bis 2 kg kostet 3,50 €, jedes Kilogramm darüber 0,80 €, und Zone 3 ist abgelegen, dort ist der Preis um 50% höher. Gib der Funktion, den Parametern und der Variable Namen, die das sagen, und mach aus den Zahlen Konstanten. Die Ausgabe muss dieselbe bleiben:
3.50 €5.90 €8.85 €Weitere Aufgaben
- Der boolesche Wert. Füge einen Parameter hinzu, der sagt, ob das Paket zerbrechlich ist (eine Gebühr von 1,20 €). Wie nennst du ihn? Wie sieht der Aufruf aus?
- Dein Code. Öffne eine alte Übung von dir. Finde drei Namen, die du heute nicht auf den ersten Blick verstehst, und ändere sie mit dem Rename des Editors. Führe das Programm aus: Ist die Ausgabe dieselbe?
- Das Wörterbuch des Shops. Schreib zehn Wörter auf, die der Mensch im Lager benutzt (Paket, Kurier, Zone …), und wie du sie im Code deiner Sprache benennen würdest.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Methoden · Parameter und return · static: was alle Objekte teilen · Exceptions
- Geht weiter in: Funktionen: eine Aufgabe, wenige Parameter, keine versteckten Seiteneffekte · Kommentare, die lügen, und Kommentare, die helfen · Code-Smells: lange Methode, große Klasse, Duplikate, Primitive statt Typen, lange if-Ketten · Tests als Sicherheitsnetz
- Aufgaben zum Thema: Sauberer Code und Refactoring