Abhängigkeiten reduzieren – Extract Method und Introduce Parameter
Der folgende Ausschnitt aus einer Wecker Anwendung zeigt den Code der ausgeführt wird, wenn in der Benutzerschnittstelle die Start Schaltfläche betätigt wird:
starten.Click += (s, e) => { if (txtWann.Text == "") { txtWann.Text = DateTime.Now.ToLongTimeString(); } var weckzeit = DateTime.Parse(txtWann.Text); Wecker_starten(weckzeit); };
Im ersten Schritt hilft ein Extract Method dabei, den Code verständlicher zu machen. Die zusätzliche Methode ermöglicht es dem Leser, allein anhand des Methodennamens zu verstehen, was beim Click Event passieren soll:
starten.Click += (s, e) => { var weckzeit = Weckzeit_ermitteln(); Wecker_starten(weckzeit); };
Die extrahierte Methode sieht wie folgt aus:
private DateTime Weckzeit_ermitteln() { if (txtWann.Text == "") { txtWann.Text = DateTime.Now.ToLongTimeString(); } var weckzeit = DateTime.Parse(txtWann.Text); return weckzeit; }
In dieser Methode möchte ich durch ein Introduce Parameter Refactoring die Abhängigkeit der Methode zum Steuerelement txtWann der Benutzerschnittstelle entfernen. Dazu markiere ich das Control txtWann inklusive der Eigenschaft Text, hier also „txtWann.Text“ und führe dann mit Introduce Parameter einen Methodenparameter ein. Die IDE schlägt vor, den Zugriff an zwei Stellen durch den Parameter zu ersetzen. Es werden lediglich zwei der drei Verwendungsstellen vorgeschlagen, da die dritte Verwendung schreibend ist. Das Resultat des Refactorings zeigt folgendes Listing:
private DateTime Weckzeit_ermitteln(string weckzeit_als_Text) { if (weckzeit_als_Text == "") { txtWann.Text = DateTime.Now.ToLongTimeString(); } var weckzeit = DateTime.Parse(weckzeit_als_Text); return weckzeit; }
Mit diesem Ergebnis bin ich nicht zufrieden, da immer noch auf das Control zugegriffen wird. Ferner hat sich hier nun die Semantik geändert! Innerhalb der Bedingung, mit der geprüft wird, ob der Text leer ist, wird die aktuelle Uhrzeit in das Control gesetzt. Dieser Wert wird dann jedoch nicht mehr vom darauffolgenden DateTime.Parse Aufruf übernommen. Ich mache das Refactoring daher rückgängig. Das Problem liegt hier darin, dass subtil zwei Aspekte vermischt sind:
- Parsen der Weckzeit aus der Benutzereingabe.
- Setzen der Textbox auf die aktuelle Zeit als Standardwert, falls keine Eingabe vorhanden ist.
Vor weiteren Refactorings trenne ich daher zunächst die beiden Aspekte. Erster Schritt ist ein Extract Method, mit dem ich das optionale Zuweisen des Standardwertes aus der Methode herausziehe:
public DateTime Weckzeit_ermitteln() { Default_setzen(); return DateTime.Parse(txtWann.Text); } private void Default_setzen() { if (txtWann.Text == "") { txtWann.Text = DateTime.Now.ToLongTimeString(); } }
Anschließend entferne ich den Aufruf von Default_setzen und verschiebe ihn an die Aufrufstelle von Weckzeit_ermitteln.
starten.Click += (s, e) => { Default_setzen(); var weckzeit = Weckzeit_ermitteln(); Wecker_starten(weckzeit); };
Nun kann ich sowohl in Default_setzen als auch in Weckzeit_ermitteln mit Introduce Parameter dafür sorgen, dass erkennbar wird, auf welchen Eingaben die Methoden arbeiten:
public DateTime Weckzeit_ermitteln(string text) { return DateTime.Parse(text); } private void Default_setzen(TextBox textBox) { if (textBox.Text == "") { textBox.Text = DateTime.Now.ToLongTimeString(); } }
In Weckzeit_ermitteln habe ich txtWann.Text durch den Parameter ersetzt. In Default_setzen musste ich aufgrund des schreibenden Zugriffs das ganze Control txtWann durch den Parameter ersetzen. Die Aufrufe sehen jetzt wie folgt aus:
starten.Click += (s, e) => { Default_setzen(txtWann); var weckzeit = Weckzeit_ermitteln(txtWann.Text); Wecker_starten(weckzeit); };
Mit diesem Ergebnis bin ich zufrieden. Nun wird der Ablauf klar erkennbar: zunächst wird auf dem Control ein Defaultwert gesetzt. Anschließend wird aus der Benutzereingabe die Weckzeit ermittelt und der Wecker mit dieser Weckzeit gestartet. Übrig bleibt die Abhängigkeit von der aktuellen Uhrzeit, repräsentiert durch DateTime.Now.
Fazit
Benutzen Sie Extract Method und Introduce Parameter, um Aspekte zu trennen und Abhängigkeiten zu reduzieren. Beachten Sie jedoch, dass diese Refactorings nicht mehr ausschließlich durch Anwenden eines Refactoringwerkzeugs möglich sind. Um sicherzustellen, dass Sie die Semantik des Programmes nicht verändern, brauchen Sie ein Sicherheitsnetz aus Versionskontrolle, automatisierten und manuellen Tests sowie Code Reviews.
Aspekte trennen – Move To Another Type
Häufig sind innerhalb einer Klasse unterschiedliche Aspekte vermischt. Dabei sind die Aspekte manchmal bereits auf mehrere Methoden verteilt, manchmal aber innerhalb der Methoden vermischt. Solange die Aspekte noch innerhalb einer Methode vermischt sind, können Sie versuchen, mit dem Extract Method Refactoring Codebereiche aus der Methode in zusätzliche Methoden auszulagern, um auf diese Weise für eine Trennung der Aspekte auf der Ebene der Methoden zu sorgen. Im Anschluss können dann die Methoden verschiedenen Klassen zugeordnet werden, um auf diese Weise auch auf der Ebene der Klasse für eine Aspekttrennung zu sorgen. Hierbei bietet sich das Move To Another Type Refactoring an. Beim folgenden Beispiel wurde ein CSV Viewer innerhalb einer einzigen Klasse, hier der Klasse Program, realisiert. Das Listing zeigt einen Ausschnitt:
class Program { static void Main(string[] args) { var rawLines = File.ReadAllLines(args[0]); var pageLen = 5; if (args.Length > 1) pageLen = int.Parse(args[1]); var pageLines = rawLines.Take(pageLen + 1); var iFirstLineOfLastPage = 1; while (true) { var records = pageLines.Select(l => Convert_line_to_record_fields(l, ",")); … } } private static string[] Convert_line_to_record_fields(string line, string delimiter) { return Convert_line_to_record_fields(line, delimiter, new List()).ToArray(); } private static List Convert_line_to_record_fields(string line, string delimiter, List fields) { if (line == "") return fields; if (line.StartsWith("\"")) { ... } return Convert_line_to_record_fields(line, delimiter, fields); } }
Die Aspekte wurden auf der Ebene der Methoden bereits teilweise getrennt. So ist das Thema Zerlegen von CSV Zeilen in zwei Methoden zusammengefasst. Diese beiden Methoden sollen nun aus der Klasse Program in eine eigene Klasse ausgelagert werden, um dadurch die Aspekte auch auf der Ebene der Klassen zu trennen.
Vor dem Herausziehen der beiden Methoden muss deren Sichtbarkeit von private zu public geändert werden. Dieser Eingriff birgt in der Regel kein Risiko. Im Anschluss markieren Sie eine der Methoden, die in eine eigene Klasse verlagert werden soll, und starten das Move To Another Type Refactoring. Je nach eingesetztem Refactoring Werkzeug bzw. IDE erfolgt dann eine Abfrage oder Auswahl der Klasse, in die die Methode verschoben werden soll. Ferner wird dabei abgefragt, welche Methoden verschoben werden sollen. Hier ist es wichtig, alle Methoden zu selektieren, die zum selben Aspekt gehören. Im Beispiel lagere ich die beiden Varianten der Methode Convert_line_to_record_fields in die Klasse CSV aus. Durch Einsatz eines Refactoring Werkzeugs, wie bspw. ReSharper, ist sichergestellt, dass die Aufrufe der Methoden so angepasst werden, dass sie nun auf die neue Klasse verweisen.
var records = pageLines.Select(l => CSV.Convert_line_to_record_fields(l, ",")); static internal class CSV { public static string[] Convert_line_to_record_fields(string line, string delimiter) { return Convert_line_to_record_fields(line, delimiter, new List()).ToArray(); } public static List Convert_line_to_record_fields(string line, string delimiter, List fields) { ... } }
Fazit
Benutzen Sie das Move Method To Another Type Refactoring, um Aspekte auf der Ebene der Klassen zu trennen. Sofern die Aspekte noch innerhalb von Methoden vermischt sind, wenden Sie zuvor das Extract Method Refactoring an, um die Aspekte auf der Ebene von Methoden zu trennen.
Weiter zu Teil 5 der Serie.
Extract Method macht definitiv Sinn. Auch um das Single Level of Abstraction einzuhalten, was in einen der vorherigen Posts ja schon erwähnt wurde.
Was mich jedoch beim obigen Beispiel noch etwas stört ist der Name der Methode
Default_setzen();
Liest man sich, ohne Kenntnisse der Methodeninhalte folgenden Block durch:
Default_setzen(txtWann);
var weckzeit = Weckzeit_ermitteln(txtWann.Text);
Wecker_starten(weckzeit);
kann man denken, dass txtWann immer vor dem Ermitteln der Weckzeit auf den Standard zurückgesetzt wird.
In solchen Fällen pack ich gern ein „_wenn_noetig“ oder ähnliches hinten dran. Damit erkennt der Leser, dass txtWann nicht immer auf den Default gesetzt wird.
Danke für den Hinweis! Ich stimme zu, der Bezeichner Default_setzen_wenn_nötig transportiert klarer, was die Methode macht.