Die bekannteste Regel über Methoden lautet: Sie sollen klein sein. Das stimmt, aber es ist die Wirkung, nicht die Ursache. Die eigentliche Regel ist:
Eine Methode tut eine Sache — und die Dinge, die sie tut, liegen auf derselben Ebene.
Kurz wird sie dabei von selbst.
public void Abschliessen(Bestellung bestellung)
{
if (bestellung.Positionen.Count == 0)
throw new InvalidOperationException("Leere Bestellung");
decimal summe = 0;
foreach (var position in bestellung.Positionen)
{
var zeile = position.Menge * position.Einzelpreis;
if (position.Menge >= 10)
zeile *= 0.95m;
summe += zeile;
}
var steuer = Math.Round(summe * 0.081m, 2, MidpointRounding.AwayFromZero);
bestellung.Gesamtbetrag = summe + steuer;
using (var befehl = _verbindung.CreateCommand())
{
befehl.CommandText = "update bestellung set betrag = @b where id = @id";
befehl.Parameters.AddWithValue("@b", bestellung.Gesamtbetrag);
befehl.Parameters.AddWithValue("@id", bestellung.Id);
befehl.ExecuteNonQuery();
}
var text = $"Ihre Bestellung über {bestellung.Gesamtbetrag:C} ist bestätigt.";
_smtp.Send(new MailMessage("shop@example.com", bestellung.Email, "Bestätigung", text));
}Diese Methode ist nicht schlecht geschrieben. Sie ist gut lesbar, wenn man sie einmal von oben nach unten liest. Das Problem zeigt sich erst bei der Frage: Auf welcher Ebene denkt sie?
- «Eine Bestellung abschliessen» — die Ebene des Namens.
- «Über Positionen summieren und ab zehn Stück 5% abziehen» — Geschäftsregel.
- «
AddWithValueauf einemDbCommand» — Datenbanktechnik. - «
MailMessagemit Absenderadresse bauen» — Versandtechnik.
Vier Ebenen in einer Methode. Wer sie liest, um die Rabattregel zu verstehen, muss an SQL-Parametern vorbei. Wer den Mailversand sucht, liest die Steuerberechnung mit.
Der Test: Kannst du sie ohne «und» beschreiben?
«Diese Methode prüft die Bestellung und rechnet die Summe und schlägt die Steuer drauf und speichert und verschickt eine Mail.» Jedes «und» in der Beschreibung ist ein Kandidat für eine eigene Methode.
Dieselbe Logik, nach Ebenen sortiert
public void Abschliessen(Bestellung bestellung)
{
PruefeNichtLeer(bestellung);
bestellung.Gesamtbetrag = BerechneGesamtbetrag(bestellung);
_bestellungen.SpeichereBetrag(bestellung.Id, bestellung.Gesamtbetrag);
_benachrichtigung.SendeBestaetigung(bestellung);
}
private decimal BerechneGesamtbetrag(Bestellung bestellung)
{
var netto = bestellung.Positionen.Sum(BerechneZeilenbetrag);
return netto + BerechneMehrwertsteuer(netto);
}
private static decimal BerechneZeilenbetrag(Position position)
{
var zeile = position.Menge * position.Einzelpreis;
return position.Menge >= MengeFuerRabatt ? zeile * RabattFaktor : zeile;
}Die oberste Methode liest sich jetzt wie eine Inhaltsangabe: prüfen, rechnen, speichern, benachrichtigen. Wer wissen will, wie gerechnet wird, geht eine Ebene tiefer. Wer es nicht wissen will, bleibt oben.
Das ist der eigentliche Gewinn — nicht, dass die Methoden kürzer sind, sondern dass man auswählen kann, wie tief man liest.
Extrahieren hat auch eine falsche Richtung
Eine zwanzigzeilige Methode in vier Fünfzeiler zu zerlegen, die nur zusammen Sinn ergeben und nur einmal aufgerufen werden, macht es schlimmer: Man springt viermal, um einen Ablauf zu verstehen, den man vorher am Stück lesen konnte. Der Schnitt muss entlang von Abstraktionsebenen verlaufen, nicht entlang von Zeilenzahlen.