Stefan Lieser
Stefan Lieser

Einfache Refactorings – Teil 3

Bei diesem Beitrag handelt es sich um eine überarbeitete Version. Er wurde zuvor bereits bei refactoring-legacy-code.net veröffentlicht.

Dies ist eine Beitragsserie. Die weiteren Beiträge finden Sie hier:

if-Anweisungen vereinfachen – Extract Method

Ein weiteres Anwendungsgebiet für das Extract Method Refactoring sind Bedingungen von Verzweigungen. Das folgende Listing zeigt einen Ausschnitt aus einer Weckeranwendung.

if (DateTime.Now >= weckZeit) {
    using (var soundPlayer = new SoundPlayer(@"c:\Windows\Media\chimes.wav")) {
        soundPlayer.Play();
    }
    timer.Stopp();
}

Die Bedingung der if-Anweisung erschließt sich beim Lesen spätestens, wenn wir den Inhalt der Verzweigung lesen. Da wird eine Sounddatei abgespielt und ein Timer gestoppt, so dass alles danach aussieht, als würde dieser Code ausgeführt, wenn die Weckzeit erreicht ist. Vergleichen Sie den ursprünglichen Code mit folgender Variante:

if (WeckzeitErreicht()) {
    using (var soundPlayer = new SoundPlayer(@"c:\Windows\Media\chimes.wav")) {
        soundPlayer.Play();
    }
    timer.Stopp();
}


private bool WeckzeitErreicht() {
    return DateTime.Now >= weckZeit;
}

Nun kann ich die Bedingung der if-Anweisung lesen und verstehe sofort den Sinn der Verzweigung. Auch hier ist wieder eine weitere Abstraktionsebene eingezogen, die es dem Leser ermöglicht, das Abstraktionsniveau beizubehalten oder in die Details abzusteigen. Der Leser hat die Wahl, was er vor dem Refactoring nicht hatte. Im Beispiel kommt es allerdings noch zu einer Verletzung des Prinzip Single Level of Abstraction. Die Bedingung der Verzweigung steht nun auf hohem Abstraktionsniveau, während der Rumpf der bedingten Ausführung sich auf niedrigem Abstraktionsniveau befindet. Ein weitere Extract Method passt die Niveaus an:

if (WeckzeitErreicht()) {
    Wecken();
}

Fazit

Benutzen Sie Extract Method, um die Bedingung einer if-Anweisung leichter verständlich zu machen. Das höhere Abstraktionsniveau macht die Bedingung für den Leser besser lesbar, vorausgesetzt, der Name ist gut gewählt. Darüberhinaus bietet sich durch Extract Method die Gelegenheit, eine DRY Verletzung zu beheben, sprich eine Dopplung kann eliminiert werden.

Eingaben sichtbar machen – Introduce Parameter

Um eine Methode verstehen zu können, müssen die Eingaben und Ausgaben der Methode bekannt sein. Der erste Blick geht in der Regel zur Signatur der Methode. Dort wird anhand des Rückgabetyps der Methode deutlich, ob die Methode ein Ergebnis liefert und von welchem Typ dieses ist. Über die Methodenparameter wird deutlich, welches die Eingaben der Methode sind. Aus den Eingabewerten entsteht durch die Methode der Ausgabewert – EVA – Eingabe, Verarbeitung, Ausgabe. Im folgenden Listing ist dies einfach ersichtlich:

public int Summe(int[] werte)

Aufgrund dieser Signatur erwarten wir von der Methode, dass sie die Werte aus dem Integerarray aufsummiert und als Ergebnis zurück liefert. Was ist Ihre Erwartung beim Anblick der folgenden Signatur?

public int Summe()

Aufgrund der Signatur alleine bleibt unklar, wie die Methode arbeitet. Mit den Kenntnissen in Objektorientierung können wir als Entwickler erahnen, dass diese Methode auf Zustand der Klasse arbeiten wird. Hier ist das gesamte Listing:

public class Calculator
{
    private readonly int[] _werte;

    public Calculator(int[] werte) {
        _werte = werte;
    }

    public int Summe() {
        var result = 0;
        foreach (var i in _werte) {
            result += i;
        }
        return result;
    }
}

Beim Lesen des vollständigen Quellcodes wird deutlich, dass die zu summierenden Werte über den Konstruktor in die Klasse reingereicht werden. Die Eingaben der Methode Summe bestehen also nicht nur aus den Parametern sondern auch aus den Feldern der Klasse. Manchmal müssen Methoden auf Zustand der Klasse arbeiten. Wo dies jedoch vermeidbar ist, sollten die Eingaben alle als Parameter übergeben werden, da die Methode auf diese Weise leichter verständlich ist. Es muss dann nämlich nicht die gesamte Klasse betrachtet werden, sondern ein isolierter Blick auf die Methode genügt. Der zu betrachtende Ausschnitt des Codes ist kleiner. Des Weiteren ist der Mechanismus einfacher. Ein Leser muss nicht den Konstruktor und das Feld kennen, bevor er versteht, wie die Methode arbeitet. Es genügt ein isolierter Blick auf die Methode.

Mit Hilfe des Introduce Parameter Refactorings kann die Methode so verändert werden, dass ihr die benötigten Werte als Parameter reingereicht werden. Dazu markieren Sie in der Methode das Feld und starten in Ihrer IDE das Introduce Parameter Refactoring.

public int Summe() {
    var result = 0;
    foreach (var i in _werte) {
        result += i;
    }
    return result;
}

Die IDE fügt einen Parameter vom Typ des Feldes, hier int[], in die Methodensignatur ein und versucht anschließend, bei allen Aufrufern der Methode Summe das Feld _werte als Parameter einzusetzen. An dieser Stelle sollte deutlich werden, dass dieses Refactoring häufig begrenzt ist auf den Sichtbarkeitsbereich einer Klasse. Innerhalb der Klasse kann die Ersetzung vorgenommen werden. Außerhalb der Klasse kann bei Aufrufern das Feld der Klasse in der Regel nur eingesetzt werden, wenn es public ist.

Ein weiteres Beispiel soll verdeutlichen, wie schwierig es ist, Methoden zu verstehen, die neben ihren Parametern auch auf Feldern der Klasse als Eingabe arbeiten:

public string[] CsvTabellieren(string[] csvZeilen) {
    _csvZeilen = csvZeilen;

    RecordsErstellen();
    int[] breiten = SpaltenbreitenErmitteln();
    TabelleErstellen(breiten);

    return _tabelle;
}

Die Methode CsvTabellieren erhält ein Array von Strings als Eingabe. Jeder String dieses Arrays ist eine CSV Zeile. Als Ergebnis liefert die Methode eine hübsch formatierte Tabelle dieser CSV Zeilen, wiederum als Array von Strings.

Die Funktionsweise der Lösung können wir aufgrund der aufgerufenen Methoden erahnen. Offensichtlich wird aus den CSV Zeilen eine Menge von Records erstellt. Anschließend werden die Breiten der einzelnen Spalten ermittelt, um dann zuletzt die Tabelle zu erstellen. Unklar ist, wie die Werte in die einzelnen Methoden übergeben werden. Die Methode RecordsErstellen hat weder Parameter noch gibt sie ein Ergebnis zurück (es sei denn, dieses wird ignoriert, was bei C# syntaktisch erlaubt ist). In der Zeile davor ist erkennbar, dass die Eingabe in einem Feld abgelegt wird. Dass es sich um ein Feld handelt, vermuten wir aufgrund der Namenskonvention: der Bezeichner beginnt mit einem Unterstrich und wird daher wohl ein Feld der Klasse sein.

Offensichtlich werden die Werte hier also über Felder der Klasse übergeben. Das Ergebnis von RecordsErstellen wird vermutlich ebenfalls über ein Feld der Klasse an SpaltenbreitenErmitteln übergeben. Bei der Methode TabelleErstellen werden dann beide Übergabemethoden, mittels Feld und mittels Parameter, noch vermischt. Das Beispiel ist konstruiert… Sie werden eine solche Vorgehensweise aber vermutlich trotzdem auch in Ihrer Codebasis finden. Durch Introduce Parameter Refactorings können die Methoden leicht so modifiziert werden, dass den Methoden alle benötigten Eingaben per Parameter übergeben werden. Das Ergebnis sind wie folgt aus:

public string[] CsvTabellieren(string[] csvZeilen) {
    _csvZeilen = csvZeilen;

    RecordsErstellen(_csvZeilen);
    int[] breiten = SpaltenbreitenErmitteln(_records);
    TabelleErstellen(breiten, _records);

    return _tabelle;
}

Nun erhalten die Methoden alle benötigten Eingaben als Parameter. Die Ausgaben von RecordsErstellen und TabelleErstellen werden jedoch noch über Felder weitergegeben. Hier hilft nun leider kein automatisiertes Refactoring, sondern die nachfolgende Änderung muss per Hand durchgeführt werden. Die Ausgaben der Methoden müssen mit return zurückgegeben werden, statt die Werte in Feldern abzulegen. Im Ergebnis sieht die Methode nach den Refactorings wie folgt aus:

public string[] CsvTabellieren(string[] csvZeilen) {
    var records = RecordsErstellen(csvZeilen);
    int[] breiten = SpaltenbreitenErmitteln(records);
    var tabelle = TabelleErstellen(breiten, records);

    return tabelle;
}

Fazit

Benutzen Sie Introduce Parameter, um möglichst alle Eingaben einer Methode in die Parameterliste aufzunehmen. Dadurch wird die Methode leichter verständlich.

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »
course
Clean Code Developer CoWorking

Online CoWorking inkl. Coaching –
Wir werden häufig gefragt, was man als Entwickler tun könne, um kontinuierlich dran zu bleiben am Thema Clean Code Developer. Unsere Antwort: Treffen Sie sich regelmäßig wöchentlich online mit anderen Clean Code Developern.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman