10 einfache und effektive Refactoring-Maßnahmen

Refactoring ist die Durchführung von Verbesserungen an Struktur und Lesbarkeit des Quellcodes unserer Anwendung, ohne dass dabei am Ablauf des Programms etwas verändert wird. Doch wie sieht das eigentlich in der Praxis konkret aus und welche grundlegenden Refactoring-Techniken lassen sich bei der Übernahme von Legacy-Projekten einsetzen?

Ich werde in diesem Artikel zehn einfache aber äußerst effektive Maßnahmen für Refactorings vorstellen.

Bevor wir loslegen …

Jedesmal wenn wir Änderungen am Quellcode vornehmen, laufen wir Gefahr, etwas bestehendes und funktionierendes kaputt zu machen. In eigenen Projekten mag dies nicht ganz so dramatisch sein, aber in Kundenprojekten kann dies fatale Konsequenzen haben.

Daher ist es oberstes Gebot, alle zu refakturierenden Komponenten im Vorfeld mit Unit- und Integrations-Tests abzudecken. Somit kann ein Fehler beim Durchführen eines Refactorings schnell aufgedeckt und korrigiert werden.

1. Rename: Sprechende Namen einführen

Schlecht oder gar falsch benamte Variablen oder Funktionen erschweren den Lesefluss im Quellcode erheblich und lassen den Leser irreführende Annahmen treffen.

Wandelt nichtssagende Variablen und Funktionen in sprechende Bezeichner um, sobald ihr deren Verwendung erfasst habt. Formuliert neuen Quellcode immer sprechend, sodass jede Variable und Funktion genau das macht, was der Leser von deren Benamung erwartet.

Vorher:

let rx: number = cw / tw;

Nachher:

let tileCountX: number = canvasWidth / tileWidth;

Der Unterschied zwischen dem richtigen Wort und dem beinahe richtigen ist der gleiche wie zwischen einem Blitz und einem Glühwürmchen.

Mark Twain

2. Less Magic: Magic Numbers und String-Literale im Code ersetzen

Zahlen und konstante String-Literale, die in Anweisungen unseres Quellcodes vorkommen, haben wenig Ausdruckskraft und lenken vom Verstehen des umliegenden Quellcodes ab. Ersetzt Magic Numbers und Literale durch sprechende Konstanten! So versteht der nächste Leser des Quellcodes sofort, was diese Werte repräsentieren.

Vorher:

let positionX: number = canvasWidth - 50;

Nachher:

const BORDER_RIGHT: number = 50;

...

let positionX: number = canvasWidth - BORDER_RIGHT;

Es wird immer gleich ein wenig anders, wenn man es ausspricht.

Hermann Hesse

3. Disclose Constant Variables: Variablen in Konstanten umwandeln

Ob sich der Wert einer Variable zur Laufzeit des Programms ändern kann oder nicht ist eine wichtige Information für den Leser. Wandelt daher Variablen deren Werte sich gar nicht verändern in klar erkennbare Konstanten um.

Dies gilt für globale, statische, nicht-statische und lokale Variablen. Als guter Stil gilt auch, Funktionsparameter wie Konstanten zu behandeln und deren Werte nicht neu zuzuweisen. Moderne Programmiersprachen unterbinden dies per default.

Vorher:

let canvas: Canvas = new Canvas();
canvas.drawLine( 0, 0, 50, 100 );
canvas.drawImage( img, 25, 50 );

Nachher:

const canvas: Canvas = new Canvas();
canvas.drawLine( 0, 0, 50, 100 );
canvas.drawImage( img, 25, 50 );

Die einzige Konstante im Universum ist die Veränderung.

Heraklit

4. Unify Redundancy: Wiederholungen eliminieren

Jede Information eines Projekts und jedes Stück Business-Logik, das in einem Programm implementiert, ist darf innerhalb des Projekts nur an einer einzigen Stelle verbaut sein. Andernfalls läuft man Gefahr, bei Änderung eines Wertes oder einer Logik-Berechnung mehrere, verstreute Codestellen anpassen zu müssen.

Das zahlt auf die Technische Schuld eines Projekts ein und stellt eine potentielle Fehlerquelle dar. Werden bei einer Änderung der Business-Logik nicht alle erforderlichen Stellen angepasst – da die ausgelassenen Stellen nicht ersichtlich waren – kommt es zu einem fehlerhaften Verhalten in der Anwendung.

Sobald wir einen im Projekt wiederholt vorkommenden Wert oder eine wiederholt vorkommende Berechnung entdecken, sollten wir sie konsequent an allen vorkommenden Stellen entfernen und durch eine einzige Konstante oder eine einzige Funktion ersetzen.

Vorher:

const angleA: number = 135.0;
const angleB: number = 75.0;

const sineA:   number = Math.sin( angleA * Math.PI / 180.0 );
const cosineA: number = Math.cos( angleA * Math.PI / 180.0 );

const sineB:   number = Math.sin( angleB * Math.PI / 180.0 );
const cosineB: number = Math.cos( angleB * Math.PI / 180.0 );

Nachher:

function degreeToRadian( angle: number ): number
{
    return ( angle * Math.PI / 180.0 );
}

...

const angleA: number = 135.0;
const angleB: number = 75.0;

const radianA :number = degreeToRadian( angleA );
const radianB :number = degreeToRadian( angleB );

const sineA:   number = Math.sin( radianA );
const cosineA: number = Math.cos( radianA );

const sineB:   number = Math.sin( radianB );
const cosineB: number = Math.cos( radianB );

Don’t repeat yourself!

Prinzip von Clean Code

5. Remove Dead Code: Auslauben

Unreferenzierte Variablen, Parameter, Funktionen und Klassen erhöhen den Umfang der Codebasis, ohne dabei einen Mehrwert zu bringen. Dadurch erhöht sich die Menge des zu wartenden Codes und der zu formulierenden Tests, um diese Codebasis abzudecken.

Je mehr nicht-benötigter Code aus der Codebasis entfernt wird, desto weniger Code kann auch potentielle Fehler produzieren. Deshalb entfernen wir nicht benötigte Werte oder Strukturen konsequent.

Vorher:

public multiply( x: number, y: number ): number
{
    const z: number = ( x / y );

    return ( x * y );
}

Wird für y der Wert 0 übergeben, so kommt es zu einem Programmabsturz aufgrund der Division durch 0. Durch das Entfernen der Zuweisung zu z kann ein möglicher Programmabsturz vermieden werden.

Nachher:

public multiply( x: number, y: number ): number
{
    return ( x * y );
}

You ain’t gonna need it!

Prinzip des Extreme Programmings

6. Descope: Wirkungsbereiche verkleinern

Je mehr Variablen und Werte wir in einem Stück Quellcode gleichzeitig überblicken müssen, desto komplexer und schwerer lesbar wird das Programm. Daher sollten wir die Wirkungsbereiche aller Variablen im Programm immer auf das minimal Nötigste reduzieren.

Die Reichweite einer lokalen Variable kann eingeschränkt werden, indem sie eine oder mehrere Codeebenen weiter eingerückt wird. Für statische oder nicht-statische Variablen kann deren Reichweite über den Visibility-Modifier eingeschränkt werden – in den meisten Sprachen existieren hier die Modifizierer public und private.

Vorher:

private renderPlayer( clipX: boolean ): void
{
    var clipTargetX: number = 70.0;

    ...

    if ( clipX )
    {
        // targetX is only used inside this block

        ...
    }

    ...
}

Nachher:

private renderPlayer( clipX: boolean ): void
{
    ...

    if ( clipX )
    {
        var clipTargetX: number = 70.0;

        ...
    }

    ...
}

Im heutigen Leben bedeutet Spielraum alles.

Oscar Wilde

7. Move: Zusammenziehen und klar abgrenzen

Manchmal stößt man auf Werte und Funktionen, die man an der vorgegebenen Codestelle oder Codestruktur einfach nicht erwarten würde. Diese Werte würde man auch in Zukunft dort weder suchen noch finden.

Begegnen wir solch eine Stelle im Quellcode, sollten wir sie an eine bessere Stelle umziehen oder – falls es Sinn ergibt – sie in eine neue, separate und sprechende Einheit auslagern. So erreichen wir, dass man das entsprechende Stück Quellcode in Zukunft schneller finden wird.

Generell sollten zusammengehörige Werte und Funktionalitäten klar abgegrenzt und gekapselt werden. Somit erreichen wir, dass Änderungen an einer Einheit weniger oder gar keine Auswirkungen auf eine andere Einheit des Programms haben. Dies erhöht die Orthogonalität des Programms.

Vorher:

const car: any = {};
car.color = 'red';
car.topSpeed = 185;
car.avgSpeed = 110;

Nachher:

const car: Car = new Car( 'red', 185, 110 );

Ein guter Zaun schafft gute Nachbarn.

Robert Frost

8. Introduce Types: Typisieren

Beim Überführen von Quellcode von einer nicht-typisierten in eine dynamisch typisierte oder statisch typisierte Programmiersprache können alle Variablen und Rückgabewerte mit einem Datentyp versehen werden. Dies funktioniert beispielsweise bei der Überführung eines Programs im Rahmen einer Modernisierung von JavaScript nach TypeScript oder von PHP 5 nach PHP 7.

Durch das Typisieren von Werten können wir leicht Stellen im (Legacy-)Code aufdecken, an denen gegen dieses Prinzip verstoßen wurde. Somit können potentielle Fehlerquellen behoben und damit schwer lokalisierbare Bugs die zur Laufzeit des Programms auftreten vermeiden werden.

Vorher:

let a = 42;

...

a = 'Hello World';

...

a = new Car( 'silver', 220, 130 );

Nachher:

let a: number = 42; // static assignment to the 'number' data type

...

a = 'Hello World'; // won't compile - 'string' can't be assigned to 'number'

...

a = new Car( 'silver', 220, 130 ); // won't compile - 'Car' can't be assigned to 'number'

Du bist nicht mein Typ!

Deutscher Volksmund

9. Simplify Control Structures: Kontrollstrukturen vereinfachen

Überladene if-else-Verzweigungen, komplizierte und am besten noch geschachtelte for-Schleifen – diese Konstrukte will kein Programmierer wiederholt lesen und verstehen müssen. Mit ein wenig Umstellung können viele komplizierte Codekonstrukte einfacher verfasst werden. So werden sie für den Leser erheblich einfacher zu lesen und zu verstehen.

Beispielsweise kann ein if-else Konstrukt immer dann in ein einfaches if umgewandelt werden, wenn einer der beiden Zweige mit einem return endet. Auch for Schleifen lassen sich in den meisten Sprachen in eine foreach Schleife umwandeln und somit einfacher lesen, sofern es sich bei dem zu durchlaufenden Wert um eine iterierbare Entität handelt.

Vorher:

private render( checkCars: boolean ): void
{
    if ( checkCars )
    {
        for ( let i: number = 0; i < this.cars.length; i++ )
        {
            this.cars[ i ].check();
        }

        ...
    }
    else
    {
        this.clipToRoad();

        return;
    }

    ...
}

Nachher:

 
private render( checkCars: boolean ): void
{
    if ( !checkCars )
    {
        this.clipToRoad();

        return;
    }

    for ( const car of this.cars )
    {
        car.check();
    }

    ...
}
 

Keep it simple, stupid.

Prinzip von Clean Code

10. Unify the Layer of Abstraction: Einheitliche Abstraktionsebenen schaffen!

In unserem Quellcode gibt es verschiedene Abstraktionsebenen: Manchmal wird eine Aufgabe mit einer einzigen Zeile API-Code ausgeführt und manchmal benötigt eine Aufgabe dafür fünf Zeilen API-Code. Auch innerhalb unseres eigenen Quellcodes stellen wir fest, dass manche Aufgaben sprechende Einzeiler sind und sich im Gegensatz dazu manche Aufgaben über mehrere Zeilen oder Anweisungen erstrecken.

Unser Quellcode wird deutlich sprechender und besser lesbar, wenn wir diese Abstraktionsebenen vereinheitlichen. Einfach gesagt gelingt uns dies, wenn wir innerhalb sprechender Funktionen dafür sorgen, dass unser Programmablauf im Code auch weiterhin sprechend bleibt.

Vorher:

private render(): void
{
    this.clipToLevelBounds();
    this.checkCollisions();

    if ( this.keySystem.isPressed( KEYS.KEY_JUMP ) )
    {
        this.jump();
    }
    if ( this.keySystem.isPressed( KEYS.KEY_LEFT ) )
    {
        this.moveLeft();
    }
    if ( this.keySystem.isPressed( KEYS.KEY_RIGHT ) )
    {
        this.moveRight();
    }
}

Nachher:

private render(): void
{
    this.clipToLevelBounds();
    this.checkCollisions();
    this.handleKeys();
}

private handleKeys(): void
{
    if ( this.keySystem.isPressed( KEYS.KEY_JUMP ) )
    {
        this.jump();
    }
    if ( this.keySystem.isPressed( KEYS.KEY_LEFT ) )
    {
        this.moveLeft();
    }
    if ( this.keySystem.isPressed( KEYS.KEY_RIGHT ) )
    {
        this.moveRight();
    }
}

Es gibt keine abstrakte Kunst – man muß immer mit etwas Konkretem beginnen. Danach kann man alle Spuren des Wirklichen entfernen.

Pablo Picasso

Refactoring ist angewandtes Wissen

Refactoring ist mehr als das Anwenden plumper Änderungsmaßnahmen am Quellcode – es ist angewandtes Wissen. Je mehr theoretische und praktische Erfahrung wir beim Refakturieren gesammelt haben, desto mehr Maßnahmen und Verbesserungen können wir für die Zukunft an unserem Quellcode anwenden. Dies beeinflusst auch die Art und Weise wie wir neuen Code formulieren.

Ich würde mich sehr freuen, wenn ich euer Interesse am Thema Refactoring wecken konnte.

Für Feedback oder Rückfragen stehe ich euch sehr gerne via christopher.stock@mayflower.de zur Verfügung!

Dieser Eintrag wurde veröffentlicht in Development und verschlagwortet mit von Christopher Stock. Permanenter Link zum Eintrag.

Über Christopher Stock

Christopher ist als Senior Developer bei der Mayflower GmbH tätig und entwickelt dort seit über sieben Jahren hochwertige Web-, Desktop- und Mobile-Applikationen, unter anderem mit Java, Swift, TypeScript, C# und PHP. Seine Freizeit verbringt er neben dem Programmieren und Designen gerne mit Fitness, Verreisen, seiner Freundin und der Umsetzung kreativer Ideen.

Für neue Blogupdates anmelden:


Schreibe einen Kommentar

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