Refactoring: Strategien zum langfristigen Verbessern von Quelltextstrukturen

Seite 3: Absicht & Abhängigkeiten

Inhaltsverzeichnis

Oft wird Refactoring angewendet, wenn Code nicht unter den Prinzipien der Lesbarkeit und Wartbarkeit entwickelt wurde. In ihm finden sich oft Bezeichner, die nicht das tun, was sie versprechen. In unserem Beispiel passiert in der Methode roll alles – außer dem versprochenen Würfelwurf. Falls sich der Sinn eines Codesegments irgendwann erschließt, hilft es diesen direkt sichtbar zu machen – zum Beispiel durch das Umbenennen der Methode. Wenn sie nun tatsächlich liefert was sie verspricht, lässt sich der Rest des Codes mithilfe der Abstraktion leichter analysieren. So ist nicht konstant der gesamte Kontext im Kopf zu behalten. Außerdem kann die nächste Person, die den Code nutzt, von der direkten Dokumentation profitieren.

Ziel ist es nicht, den besten Code für den Methodennamen zu finden, da er später sowieso umstrukturiert wird. Vielmehr soll die Analyse vereinfacht werden, ohne das existierende Verhalten zu beeinträchtigen.

Es ist nicht immer einfach, die besten Bezeichner zu finden. Die Fähigkeit, gute Namen zu vergeben, lässt sich allerdings durch bewusstes Training verbessern. Auch ist es oft einfacher, die Namensgebung mit einer weiteren Person (zum Beispiel in Paarprogrammierung) zu besprechen und die eigenen Annahmen über die Verständlichkeit so zu verifizieren. Idealerweise sollte der Code sogar für Menschen lesbar sein, die selbst keinen Code schreiben.

Viele Entwicklungsumgebungen wie IntelliJ IDEA ermöglichen es, sogenannte "Magische Zahlen" (Werte, deren Bedeutung nicht unmittelbar klar ist) automatisiert in besser benannte Konstanten zu extrahieren. Es ist zu empfehlen, diese und andere automatisierten Refactoring-Schritte zu verwenden, wann immer es möglich ist. Damit lässt sich das Risiko für Fehler erheblich verringert.

int[] places = new int[6];
int[] purses = new int[6];
boolean[] inPenaltyBox = new boolean[6];

Im erwähnten Spiel lässt sich die Zahl sechs als maximale Anzahl von Spielern betrachten, und sich das entsprechende Wissen in einer treffend benannten Konstante festhalten.

int[] places = new int[MAXIMUM_AMOUNT_OF_PLAYERS];
int[] numberOfGoldCoins = new int[MAXIMUM_AMOUNT_OF_PLAYERS];
boolean[] inPenaltyBox = new boolean[MAXIMUM_AMOUNT_OF_PLAYERS];

Wichtig ist hierbei, Annahmen und die verwendeten Begriffe mit den Produktverantwortlichen abzugleichen. Eventuell ist die maximale Anzahl eine andere und ein potenzieller Fehler (oder falscher Begriff) wurde entdeckt. Dabei lässt sich gut erkennen, dass Refactoring ohne Verantwortliche oder Nutzer ein schweres Unterfangen ist, da niemand die Annahmen des Entwicklungsteams verifizieren kann.

Beim Anwenden dieses Schritts besteht die Gefahr, sich sofort auf andere Möglichkeiten der Codeverbesserung zu stürzen. Es gibt jedoch einen Grund, sich jeweils nur auf eine Methode zu beschränken: Der damit einhergehende Fokus befreit das Gehirn von der Last, zwischen den Aufgaben zu wechseln. Die Qualität der Ergebnisse steigt.

Sind externe Ressourcen involviert, fällt das Testen des Systems schwerer. Bei ihnen kann es sich zum Beispiel um Datenbanken, das Dateisystem, andere Systeme, die Konsoleneingabe, die Ausgabe oder die Systemzeit handeln. Ein erster Schritt, um das Gesamtkonstrukt mit automatisierten Tests abdecken zu können, ist diese Abhängigkeiten zu isolieren – besonders, wenn sie überall im Code verstreut sind.

Im Beispiel lässt sich eine neu entwickelte Klasse für die Konsolenkommunikation einführen:

public class ConsolePrinter implements Printer {
@Override
public void print(Object objectToPrint) {
System.out.println(objectToPrint);
}
}

Sie ist beim Erstellen der Game-Klasse zu injizieren (sogenannte Dependency injection) und später anstelle der direkten Konsolenkommunikation zu verwenden:

public Game(Printer printer) {
this.printer = printer;
...
}

public void executeMove(int roll) {
...
print("They have rolled a " + roll);
...
}

private void print(Object output) {
printer.print(output);
}

Die neu erstellte Abstraktion lässt sich nun dazu verwenden, Systemtests unabhängig von der externen Ressource zu automatisieren. Dazu injiziert das Programm eine Instanz der zu testenden Klasse und ersetzt die Konsolenkommunikation durch eine Implementierung, die im Test messbar ist:

private Game game;
private List<String> printedLines;
private Printer printer = new Printer() {
@Override
public void print(Object objectToPrint) {
printedLines.add(objectToPrint.toString());
}
};

@Before
public void initialise() {
printedLines = new ArrayList<>();
game = new Game(printer);
}

Die automatisierten Tests lassen sich nun dazu verwenden, das System zu charakterisieren und das Verhalten auf lesbare Art und Weise mit Beispielen zu dokumentieren. Gleichzeitig sichern sie Strukturmodifikationen gegen Verhaltensänderungen ab.

Viele Testframeworks erlauben parametrisierte Tests, um mit gleichbleibendem Testcode und unterschiedlichen Kombinationen von Eingangswerten mehrere Tests zu erzeugen.

Im nächsten Listing führt das System einen Teil des Codes aus und testet das erwartete Verhalten für verschiedene Würfelwürfe in der Eingabe:

@Test
@Parameters({"0,Pop", "1,Science", "2,Sports", "3,Rock"})
public void
prints_game_moves_and_asks_categorised_questions(
int rolled, String category) {
addPlayers(2);

game.executeMove(rolled);

assertThat(printedLines, contains(
"player1 was added",
"They are player number 1",
"player2 was added",
"They are player number 2",
"player1 is the current player",
"They have rolled a " + rolled,
"player1's new location is " + rolled,
"The category is " + category,
category + " Question 0"
));
}

Jeder Wurf entspricht einer Fragenkategorie: 0 führt zu "Pop" in der Ausgabe, 1 zu "Science" und so weiter. Der Testname beschreibt, welches Verhalten er testet – so ist der Grund einer fehlgeschlagenen Überprüfung schneller erkennbar. Eine Sammlung dieser Tests lässt sich auch als Diskussionsgrundlage mit Produktexperten nutzen.

Eine weitere Methode zur Dokumentation von Annahmen ist das sogenannte "Property Based Testing". Dabei definieren die Tester Annahmen und Rahmenbedingungen, die für gewisse Gruppen von Eingabewerten gelten. Mit zufälligen Werten aus dieser Gruppe lässt sich dann das Verhalten des Programms überprüfen.