Einfache vs. komplexe Refactorings
In dieser Reihe von Blogbeiträgen stelle ich Ihnen sogenannte Einfache Refactorings vor. Im Gegensatz zu Komplexen Refactorings sind diese vollständig toolgestützt durchführbar. Das bedeutet, dass Sie die Veränderungen am Quellcode automatisiert mithilfe einer IDE, z.B. Microsoft Visual Studio oder JetBrains Rider, ausführen. Vielleicht setzen Sie auch ein zusätzliches Refactoring Tool ein wie JetBrains ReSharper. Durch Einsatz eines solchen Werkzeugs bleibt das Verhalten der Anwendung beim Refactoring erhalten. Solange Sie die Refactorings konsequent ausschließlich mit solchen Werkzeugen ausführen und nicht per Hand eingreifen, ist die Wahrscheinlichkeit sehr hoch, dass sich der Code hinterher genauso verhält wie vor dem Refactoring.
Komplexe Refactorings dagegen erfordern „Handarbeit“, also ein Eingreifen ohne die Sicherheit, dass durch den Werkzeugeinsatz die Semantik erhalten bleibt. Ferner sind komplexe Refactorings in der Regel so umfangreich, dass die Auswirkungen nicht von Beginn an absehbar sind. Typischerweise fängt man an einer Stelle an und stellt fest, dass immer weitere Auswirkungen hinzukommen, die man anfangs nicht vorhergesehen hatte. Sehr schnell steht man dann mit zahlreichen losen Enden da und weiß nicht, ob die Anwendung noch funktioniert. Oder man weiß, dass etwas nicht mehr funktioniert, hat aber keine Ahnung, welche der Änderungen dazu geführt hat.
Das Ziel von einfachen Refactorings
Einfache Refactorings dienen in der Regel dazu, die Lesbarkeit und damit die Verständlichkeit des Codes zu verbessern. Das Ziel ist es, die Wandelbarkeit des Codes wieder herzustellen bzw. diese zu verbessern. Es geht nicht an erster Stelle darum, im Code Patterns, also Entwurfsmuster, herzustellen. Refactoring ist kein Selbstzweck sondern muss dem Kunden bzw. dem Unternehmen einen Nutzen bringen. Dieser Nutzen besteht in der Wandelbarkeit der Software. Wenn ein Pattern der Wandelbarkeit dient, spricht nichts dagegen, es anzuwenden. Nur ist eben nicht das Pattern das primäre Ziel, sondern die Wandelbarkeit.
Die Lesbarkeit verbessern – Rename Refactoring
Eines der einfachsten und dennoch wichtigsten Refactorings zum Verbessern der Lesbarkeit, ist das Umbenennen von Symbolen. Ob es eine Variable, ein Parameter, ein Feld, eine Eigenschaft, eine Methode oder eine Klasse ist: ein guter Name transportiert die Bedeutung und erhöht damit die Verständlichkeit des Codes.
Darüber hinaus müssen Namen in einer Codebasis einen einheitlichen Stil einhalten, weil auch dadurch die Lesbarkeit verbessert wird. Codekonventionen legen für ein Softwaresystem fest, wie bspw. Variablen, Parameter und Felder benannt werden. Neben einer einheitlichen Domänensprache muss festgelegt werden, ob und wie bspw. Variablen von Feldern durch eine Konvention unterschieden werden. So können Felder bspw. mit einem vorangestellten Unterstrich benannt werden, damit sie sich innerhalb von Methoden deutlich von lokalen Variablen und Parameter absetzen. Wie auch immer Ihre konkrete Codekonvention aussieht, wichtig ist, dass sich Ihr Team auf eine solche verständigt und sie dann im Code konsequent einhält. An Stellen, wo die Konvention nicht eingehalten wurde, hilft das Rename Refactoring.
Ein Beispiel
Im folgenden Beispiel sind die Felder item1 und item2 nicht von den Parametern des Konstruktors zu unterscheiden. Daher muss hier durch das vorangestellte this geklärt werden, welcher Bezeichner gemeint ist.
public class Pair<T1, T2> { private T1 item1; private T2 item2; public Pair(T1 item1, T2 item2) { this.item1 = item1; this.item2 = item2; } }
Nichts spricht dagegen, den Code so zu belassen, da er syntaktisch korrekt ist und die Bedeutung ebenfalls klar ist. Durch eine Codekonvention wie „Namen von Feldern einer Klasse beginnen mit einem Unterstrich“ könnte auf andere Weise die Eindeutigkeit erreicht werden. Das Team sollte sich lediglich auf eine Konvention verständigen, um Einheitlichkeit in der Codebasis zu erreichen und damit die Lesbarkeit zu erhöhen. Nach einem Rename Refactoring auf den Feldern item1 und item2 würde das Beispiel dann wie folgt aussehen:
public class Pair<T1, T2> { private T1 _item1; private T2 _item2; public Pair(T1 item1, T2 item2) { _item1 = item1; _item2 = item2; } }
Der Vorteil des toolgestützten Rename Refactorings gegenüber einem Ändern „per Hand“ im Editor liegt darin, dass die IDE auf diese Weise sicherstellt, dass alle Verwendungsstellen des Symbols angepasst werden. So schleichen sich keine Fehler ein. Die IDE sorgt beim Umbenennen natürlich auch dafür, dass Bezeichner eindeutig bleiben. Sollten Sie versuchen, ein Symbol nach einem schon vorhandenen zu benennen, erhalten Sie einen Hinweis darauf, dass der Name bereits existiert.
Sollte es Ihnen schwer fallen, für eine Funktionseinheit, das kann eine Methode oder Klasse sein, einen guten Namen zu finden, könnte dies ein Hinweis darauf sein, dass die Aufgabe der Funktionseinheit unklar ist. Häufig sind Aspekte vermischt, so dass man beim Namen für die Funktionseinheit gerne die Konjunktion „und“ verwenden würde. Durch ein Rename Refactoring lässt sich das eigentliche Problem, die Vermischung von Aspekten, nicht lösen. Zu diesem Problem komme ich später zurück.
Fazit
Benutzen Sie das Rename Refactoring, um die Bezeichner in Ihre Codebasis an Ihre Codekonventionen anzupassen und um treffende Begriffe aus der Domänensprache zu verwenden. Das erhöht die Lesbarkeit und Verständlichkeit des Codes und führt damit zu besserer Wandelbarkeit.
Die Lesbarkeit verbessern – Introduce Variable
Häufig ist es nützlich, einen Ausdruck in einer Variable abzulegen, um damit die Möglichkeit zu nutzen, einen weiteren Namen zu vergeben und damit den Ausdruck zu bezeichnen. So kann die Bedeutung des Ausdruck benannt werden und der Leser muss sich diese nicht selbst durch Lesen des Ausdrucks erarbeiten.
public double Endpreis(int anzahl, double netto) { return (anzahl * netto) * 1.19; }
Hier kann das Einführen von Variablen dabei helfen, die Berechnung schneller zu verstehen:
public double Endpreis(int anzahl, double netto) { var nettoSumme = anzahl * netto; var bruttoSumme = nettoSumme * 1.19; return bruttoSumme; }
Und natürlich kann dieses Refactoring weiter betrieben werden, um die magische Zahl „1.19“ zu benennen:
public double Endpreis(int anzahl, double netto) { const double mwSt = 1.19; var nettoSumme = anzahl * netto; var bruttoSumme = nettoSumme * mwSt; return bruttoSumme; }
Ein weiteres typisches Szenario für die Einführung einer Variable sind eingeschachtelte Methodenaufrufe. Folgendes Beispiel zeigt drei eingeschachtelte Methodenaufrufe. Zum Verständnis des Codes ist es erforderlich, diesen von innen nach außen, also von rechts nach links, zu lesen.
public IDictionary<string, string> ToDictionary(string configuration) { return InsertIntoDictionary(SplitIntoKeyValuePairs(SplitIntoSettings(configuration))); }
Im Lesen dieser eingeschachtelten Aufrufen haben wir als Entwickler einige Übung. Und dennoch entspricht es nicht unserem üblichen Lesefluss, der von oben nach unten und von links nach rechts erfolgt. Um die Aufrufe einen nach dem anderen untereinander anzuordnen, markieren Sie die einzelnen Aufrufe und starten jeweils das Introduce Variable Refactoring. Als Ergebnis erhalten Sie folgende Version der Methode:
public IDictionary<string, string> ToDictionary(string configuration) { var settings = SplitIntoSettings(configuration); var pairs = SplitIntoKeyValuePairs(settings); var result = InsertIntoDictionary(pairs); return result; }
Nun ist die Methode ToDictionary leichter zu lesen und damit verständlicher formuliert.
Fazit
Benutzen Sie Introduce Variable, um einem Ausdruck oder einem Zwischenergebnis eine Bezeichnung zu geben. Durch Einführung zusätzlicher Bezeichner erschließt sich dem Leser die Bedeutung des Codes leichter. Durch Introduce Variable können Sie ferner eingeschachtelte Aufrufe auflösen, so dass der Code von oben nach unten und von links nach rechts gelesen werden kann.
Weiter zu Teil 2 der Serie.
Hallo Herr Lieser,
tolle Beispiele für Einfach Refactorings.
Allerdings habe ich folgende Frage zum Mehrwertssteuer Beispiel:
Wäre es nicht lesbarer (in diesem speziellen Fall) die Mehrwertsteuer als 0.19 festzusetzen, da es in der Realität eig so auch der Fall ist?
Beispiel:
public double Endpreis(int anzahl, double netto) {
const double mwSt = 0.19;
var nettoSumme = anzahl * netto;
var steuer = nettoSumme * mwSt;
var bruttoSumme = nettoSumme + steuer;
return bruttoSumme;
}
Hallo Herr Lieser,
tolle Beispiele für Einfache Refactorings.
Allerdings habe ich folgende Frage zum Mehrwertssteuer Beispiel:
Wäre es nicht lesbarer (in diesem speziellen Fall) die Mehrwertsteuer als 0.19 festzusetzen, da es in der Realität eig so auch der Fall ist?
Beispiel:
public double Endpreis(int anzahl, double netto) {
const double mwSt = 0.19;
var nettoSumme = anzahl * netto;
var steuer = nettoSumme * mwSt;
var bruttoSumme = nettoSumme + steuer;
return bruttoSumme;
}
Hallo Herr Gießwein,
ja dem kann ich mich anschließen, mit 0.19 wird es vermutlich klarer. Oder man könnte auch eine Extension Method verwenden und schreiben
const double mwSt = 19.Prozent();
Die Methode Prozent() würde den Wert durch 100 teilen.
Hallo
es ist ungewöhnlich das Ergebnis einer Multiplikation Summe zu nennen 🙂
vielleicht besser
var gesamtNettoPreis = anzahl * nettoPreis