| 

.NET C# Java Javascript Exception

0
Bernd Schiffer und ich haben eine Code-Kata mit TDD programmiert. Nach ein paar Refactorings hatten wir Groovy-Code, der ungefähr so aussah (ich habe das konkrete Beispiel gegen ein einfacheres ausgetauscht): def toString(number, range = (1..number).reverse()) { if (range.empty) return "" def item = range.last() "$item" + toString(number, range - item) } assert '1' == toString(1) [...]

Bernd Schiffer und ich haben eine Code-Kata mit TDD programmiert. Nach ein paar Refactorings hatten wir Groovy-Code, der ungefähr so aussah (ich habe das konkrete Beispiel gegen ein einfacheres ausgetauscht):
def toString(number, range = (1..number).reverse()) {
 if (range.empty) return ""
 def item = range.last()
 "$item" + toString(number, range - item)
}

assert '1' == toString(1)
assert '12' == toString(2)
assert '123' == toString(3)
assert '1234567891011' == toString(11)

"toString" liefert einen String mit den Zahlen 1 bis "number". In "range" finden sich die jeweils noch zu bearbeitenden Zahlen. Das kann man in Groovy natürlich viel einfacher hinschreiben:

def toString(number) {
	(1..number).join() 
}


Aber für den Punkt dieses Blog-Beitrags bleiben wir bei der ursprünglichen Implementation. Wir bleiben also bei der "range" und fokussieren auf ein anderes Problem. Wir verdrehen bei der Erzeugung der Range die Reihenfolge mit "reverse" und greifen hinterher mit "last" darauf zu. Es wäre einfacher, wenn wir die Reihenfolge unverändert ließen und mit "first" auf das jeweils erste Element zugreifen würden. Wir brauchen also ein Refactoring, das diese beiden Stellen im Code ändert.

Dummerweise steht der Bernd davor. Er ist dagegen, dass wir einfach diese zwei Stellen im Code ändern. Er meint, das sei nicht der "grüne Weg", weil nach der ersten Änderung (entfernen von "reverse") die Tests (asserts) nicht mehr laufen. "Aber alle zwei Änderungen finden in derselben Methode statt. Das kann man doch problemlos in einem Schritt ändern und dann ist alles grün. So ist es doch viel schneller." Aber Bernd bleibt dabei. Das ist ihm zu großschrittig (ein Schulungsteilnehmer sagte einmal sinngemäß "Ich dachte immer kleiner als Elektron geht nicht, aber jetzt kenne ich den Bernd-Schritt als kleinstes Element."):

  1. Wir arbeiten an einer Code-Kata. Dabei geht es darum, dass wir unsere Programmierfähigkeiten verbessern. Es geht nicht darum, die Kata in möglichst kurzer Zeit durchzuführen. Es geht darum, sie möglichst gut durchzuführen - wie bei einer Karate-Kata.
  2. Wir stehen in der Praxis immer wieder vor großen Refactorings, die wir nicht in kleine Schritte zerlegt bekommen und die uns den letzten Nerv kosten. Je kleinschrittiger wir vorgehen können, umso besser können wir große Refactorings zerlegen. Wenn wir es schaffen, dass die Tests nach jeder Änderung eines Ausdrucks grün sind, dann können wir für jedes große Refactoring einen komplett grünen und damit sicheren Weg beschreiten.
  3. Wenn wir mehrere Änderungen auf einmal durchführen, laufen wir immer Gefahr, versehentlich Funktionalität zu erweitern - die dann nicht durch Tests abgedeckt ist. Wenn wir jeweils nur einen Ausdruck ändern, können wir normalerweise sehr sicher abschätzen, ob wir die Funktionalität erweitert haben.

"OK, Bernd hat Recht. Versuchen wir es Kleinstschrittig. Äh, geht das denn überhaupt?"

Ja, es geht. Gleicht kommt die Lösung. Wer sich selbst an der Aufgabe versuchen möchte, sollte hier nicht weiterlesen und es erstmal selbst versuchen. Die Regel: Einen Ausdruck ändern, Tests ausführen, es muss alles grün sein. Danach der nächste Ausdruck.

Eine Lösung
Es gibt verschiedene Möglichkeiten, das Problem zu lösen. Hier eine mögliche Lösung:

Zuerst vereinfache ich den "range"-Defaultwert, indem ich das umdrehen der Elemente bereits beim Hinschreiben erledige:

def toString(number, range = number..1) {
 if (range.empty) return ""
 def item = range.last()
 "$item" + toString(number, range - item)
}


Jetzt kommt der interessante Teil. Die Reihenfolge der Elemente in "range" hängt direkt mit dem Zugriff über "last" zusammen. Wenn ich "last" durch "first" ersetzen möchte, muss ich dafür sorgen, dass in "range" am Anfang und Ende dasselbe Element steht. Das erreiche ich, indem ich "range" quasi verdoppele:
def toString(number, range = (1..number)+(number..1)) {
 if (range.empty) return ""
 def item = range.last()
 "$item" + toString(number, range - item)
}


Dass "range" jetzt jede Zahl doppelt enthält, schadet nicht, weil "range - item" alle "item"s entfernt. Mit diesem verdoppelten Range liefert "last" dasselbe Ergebnis wie "first", so dass ich die Ersetzung problemlos vornehmen kann.
def toString(number, range = (1..number)+(number..1)) {
 if (range.empty) return ""
 def item = range.first()
 "$item" + toString(number, range - item)
}


Jetzt kann ich den Default-Wert für "range" wieder vereinfachen:
def toString(number, range = 1..number) {
 if (range.empty) return ""
 def item = range.first()
 "$item" + toString(number, range - item)
}


Und fertig bin ich mit meinem Refactoring. Wir haben nach jeder Änderung eines Ausdrucks die Tests durchlaufen lassen und sie waren immer grün. In der Praxis würde man in diesem Beispiel vielleicht nicht so kleinschrittig vorgehen. Aber hier geht es um den Trainingseffekt!

Eine interessante Frage ist sicherlich, ob es immer solche kleinschrittigen Auflösungen gibt oder ob es Fälle gibt, wo das nicht funktioniert. Wenn Ihr Refactoring-Beispiele habt, von denen Ihr glaubt, dass es keine Kleinstschrittige Zerlegung gibt, schickt mir die! Entweder an stefan AT stefanroock DOT de (AT durch @ und DOT durch . ersetzen) oder als Kommentar an diesen Blog-Beitrag.


Tagged: Refactoring, Testing
testing refactoring
Weitere News:
Schreibe einen Kommentar:
Themen:
refactoring testing
Entweder einloggen... ...oder ohne Wartezeit registrieren
Benutzername
Passwort
Passwort wiederholen
E-Mail