Schneller durch TDD

Sorry, this entry is only available in German. For the sake of viewer convenience, the content is shown below in the alternative language. You may click the link to switch the active language.

Manche Studien sagen, dass testgetrieben entwickelter Code besser ist (z.B. Empirical Studies Show Test Driven Development Improves Quality).
Etliche Studien sagen, dass es mehr Aufwand ist, Code testgetrieben zu entwickeln (z.B. 15-35% laut Evaluating the Efficacy of Test-Driven Development: Industrial Case Studies).

Ich werde durch TDD schneller – wirklich!

In einem früheren Artikel über Programmierwettbewerbe habe ich beschrieben, dass wir am 18.12. mit der “Catalysts Christmas Challenge” einen öffentlcihen Programmierwettbewerb mit 62 Teilnehmern abgehalten haben; und dass wir eine Woche davor einen internen Programmierwettbewerb als Probelauf abgehalten haben.

Der interne Programmierwettbewerb war für 9:00 angesetzt. Ich habe in der Früh noch einen Arzttermin mit einem unserer Kinder zu erledigen gehabt, habe dann noch schnell einkaufen fahren müssen und kurz vor 9 Uhr Christian Federspiel (er hat diesmal das Beispiel entworfen und den Wettbewerb organisiert) angerufen, dass ich mich wohl verspäten werde. Christian hat gemeint, das mache nichts, weil er auch noch im Stau (von Goldwörth nach Linz) stecke und dass der Wettbewerb auf 9:30 verschoben sei.
Ich komme um 9:30 ins Büro, der Wettbewerb hat gerade begonnen.
Ich haste zum Tisch, packe meinen Computer aus und stecke ihn an.
Ich suche auf meinem Desktop nach dem Eclipse-Icon und starte Eclipse.
Außer Atem setze ich mich hin und steige mit ein paar Minuten Verspätung in den Wettbewerb ein.

Es schaut schlecht für mich aus: ich komme neben den vielen anderen Arbeiten bei weitem nicht mehr so viel zum Programmieren wie viele andere bei Catalysts, ich habe den Kopf voller Dinge, ich komme zu spät, ich weiß nicht einmal mehr, ob ich eine aktuelle Version von Eclipse installiert habe…

Aber ich mache “gute Miene zum bösen Spiel”, ich programmiere mit.

Aber ich mache es auf meine Art, testgetrieben.

Ich lese mir die Angabe durch und schreibe den ersten Test (das ist 1:1 mein Source Code ohne Nachbesserungen):

public class TestLevel1 extends TestCase {
  public void testParse() {
    Level1 level1 = new Level1();
    level1.parse("6 3 1 6 5 -2 4");
    assertEquals(level1.numbers.length, 6);
  }
}

Ich implementiere die parse-Methode:

public class Level1 {
public int[] numbers;

public void parse(String input) {
  String[] strings = input.split(" ");
  int length = Integer.parseInt(strings[0]);
  numbers = new int[length];
  for (int i = 0; i < length; i++) {     numbers[i] = Integer.parseInt(strings[i + 1]);   } }

Ich schreibe den zweiten Test, der sich zwingend aus der Angabe ergibt:

public void testIsOrientedPair1(String input) {   Level1 level1 = new Level1();   level1.parse(input);   assertEquals(level1.numbers.length, 6);   assertFalse(level1.isOrientedPair(6, 3));   assertTrue(level1.isOrientedPair(1, -2));   ArrayList  pairs = level1.findOrientedPairs();   assertEquals(2, pairs.size());   assertEquals("2 1 -2 3 -2", level1.printPairs(level1.sortedPairs(pairs))); }

Und ich implementiere die Methode findOrientedPairs. Dabei ist auch die Methode isOrientedPair entstanden:

public boolean isOrientedPair(int x, int y) {   if (x >= 0 && y >= 0) return false;
  if (x < 0 && y < 0) return false;
  int diff = Math.abs(x) - Math.abs(y);
  if (diff == -1 || diff == 1)
    return true;
  return false;
}

public ArrayList
 findOrientedPairs() {
  ArrayList
 pairs = new ArrayList
();
  for (int i = 0; i < numbers.length; i++) {
    int x = numbers[i];
    for (int j = i + 1; j < numbers.length; j++) {
      int y = numbers[j];
      if (isOrientedPair(x, y))
        pairs.add(new Pair(x, y));
    }
  }
  return pairs;
}

Und dann war blöderweise auch noch gefordert, dass die orientierten Paare im Ergebnis aufsteigend sortiert ausgegeben werden sollten.
Ich weiß, dass ich nicht mehr weiß, wie ich eine Collection sortieren lassen kann. Ich weiß auch, dass ich auf die Schnelle (immerhin geht es bei einem Programmierwettbewerb darum, die Lösung SCHNELL zu programmieren) nicht nachlesen und lernen kann, wie das geht.
Das heißt, ich muss das Ergebnis händisch sortieren – ich denke mir, dass es wohl mit einem Insertion Sort am schnellsten ginge, somit entstehen die Methoden sortedPairs und printPairs:

public ArrayList
 sortedPairs(ArrayList
 pairs) {
  ArrayList
 sorted = new ArrayList
();
  for (int i = 0; i < pairs.size(); i++) {
    Pair p = pairs.get(i);
    int pos = 0;
    for (int j = 0; j < sorted.size(); j++) {       Pair q = sorted.get(j);       if (p.x > q.x) {
        pos = j;
        break;
      }
    }
    sorted.add(pos, p);
  }
  return sorted;
}

public String printPairs(ArrayList
 pairs) {
  StringBuilder output = new StringBuilder();
  output.append(pairs.size());
  for (int i = 0; i < pairs.size(); i++) {
    Pair p = pairs.get(i);
    output.append(" " + p.x + " " + p.y);
  }
  return output.toString();
}

Auch diese Methoden ergaben sich zwingend aus dem Test.

Zum Abschluss des Levels 1 habe ich dann noch das weitere Beispiel in einen Testfall gegossen und die Ausgabe an den CatCoder geschickt:

public void testIsOrientedPair2(String input) {
  Level1 level1 = new Level1();
  level1.parse(input);
  ArrayList
 pairs = level1.findOrientedPairs();
  System.out.println(level1.printPairs(level1.sortedPairs(pairs)));
}

Schwuppsdiwupps war der erste Level geschafft.
Und das Coole an der ganzen Sache: wie ich Tests schreibe und ausführe weiß ich noch, ansonsten kenne ich mich mit Eclipse nicht mehr so super aus. Dennoch kann ich wegen TDD fokussiert programmieren. Vergessen ist, dass ich gerade noch im Krankenhaus war und dass ich vom Einkaufen ins Büro gehetzt bin.

Im Nachhinein erfahre ich, dass beim öffentlichen Wettbewerb ein Teilnehmer krampfhaft nach den Collection-Sort-Methoden gesucht hat – ich hab das erst gar nicht gesucht, sondern einfach schnell ausprogrammiert, auch wenn’s mich 15 Zeilen und wahrscheinlich 10 Minuten gekostet hat.

Weiter geht’s mit dem Level 2 – der Test ist schnell geschrieben:

public void testLevel21() {
  Level2 level2 = new Level2();
  level2.parse("8 0 3 1 6 5 -2 4 7 1 2 -2 5");
  assertEquals("0 3 1 2 -5 -6 4 7",
  level2.printPairs(level2.invertPermutation(level2.numbers, level2.pair)));
 }

Aber dann ist Schluss – nach knapp einer Stunde ist der (auf eine Stunde begrenzte) interne Wettbewerb für mich zu Ende, ich hab Level 2 in 1:05 geschafft, aber nicht mehr. Drei meiner Kollegen sind mit Level 4 fertig (Klaus Gieber, Wolfgang Ebner, Dominik Hurnaus).

Zur Info noch mein Source Code vom Level 2 (ohne die trivialen Methoden parse und printPairs):

public static int[] invertPermutation(int[] numbers, Pair p) {
  int posX = findPosition(numbers, p.x);
  int posY = findPosition(numbers, p.y);
  int[] iNumbers = new int[numbers.length];
  int from, to;
  if (p.x + p.y == 1) {
    from = posX;
    to = posY - 1;
  } else {
    from = posX + 1;
    to = posY;
  }

  int j = 0;
  for (int i = 0; i < from; i++) {     iNumbers[j] = numbers[i];     j++;   }   for (int i = to; i >= from; i--) {
    iNumbers[j] = -numbers[i];
    j++;
  }
  for (int i = to + 1; i < numbers.length; i++) {
    iNumbers[j] = numbers[i];
    j++;
  }
  return iNumbers;
}

private static int findPosition(int[] numbers, int x) {
  int i = 0;
  while (i <= numbers.length && numbers[i] != x) i++;   return i; }

Es hat mir zwar getaugt, aber irgendwie denke ich mir, dass ich wohl doch schon zum alten Eisen gehöre, weil die anderen viel schneller waren als ich. Am Wochenende setze ich mich um 21:10 wieder zum Computer und schau mir die Angabe von Level 3 an. Es war schon ein langer anstrengender Tag, aber was soll’s, es interessiert mich halt, wie’s weitergeht.

public void testLevel31() {   Level3 level3 = new Level3();   level3.parse("8 0 3 1 6 5 -2 4 7 1 2 -2 5");   int[] numbers = Level2.invertPermutation(level3.numbers, level3.pair);   level3.numbers = numbers;   assertEquals(2, level3.getScore()); }

Und siehe da, ich kann ja Vieles aus Level 2 wiederverwenden. Die Lösung von Level 3 sind nur ein paar Zeilen (plus halt wieder die triviale parse-Methode):

public int getScore() {   Level1 level1 = new Level1();   level1.numbers = this.numbers;   ArrayList  pairs = level1.findOrientedPairs();   return pairs.size(); }

Jetzt denke ich mir, wenn das so leicht geht, mache ich den Level 4 auch noch – her mit dem Test:

public void testLevel41() {   Level4 level4 = new Level4();   level4.parse("8 0 3 1 6 5 -2 4 7");   assertEquals(5, level4.getNumberOfInversions()); }

Und her mit der Lösung:

public int getNumberOfInversions() {   Level1 level1 = new Level1();   Level2 level2 = new Level2();   level1.numbers = this.numbers;   ArrayList  pairs = level1.findOrientedPairs();   int iterationCount = 0;   while (pairs.size() > 0) {
    int indexOfMaxScore = -1;
    int maxScore = -1;
    for (int i = 0; i < pairs.size(); i++) {       Level3 level3 = new Level3();       level3.numbers = level1.numbers;       level3.pair = pairs.get(i);       int score = level3.getScore();       if (score > maxScore) {
        maxScore = score;
        indexOfMaxScore = i;
      }
    }
    level1.numbers = level2.invertPermutation(level1.numbers,
      pairs.get(indexOfMaxScore));
    pairs = level1.findOrientedPairs();
    iterationCount++;
   }
  return iterationCount;
}

Da kann ich jetzt zwar auch wieder Code von den früheren Levels wiederverwenden, aber wirklich schön ist es nicht mehr.
Da würde jetzt etwas Refactoring anstehen, aber zuerst schaue ich noch, ob auch die anderen Tests funktionieren.

Um 21:35 bin ich fertig. Das macht dann in Summe 1:05 + 0:30 = 1:35.

Am Tag der Catalysts Christmas Challenge bin ich dann schon ziemlich erstaunt über meine “Leistung”, weil ich mit dieser Zeit doch tatsächlich an 5. Stelle (von 62 Teilnehmern) gelandet wäre.


Ich bin kein Super-Programmierer.
Und ich habe nicht mehr viel Zeit zum Programmieren.
Aber ich weiß, wie man testgetrieben entwickelt.
Und ich entwickle immer testgetrieben,

  • weil ich dadurch sicherer werde
  • weil ich dadurch fokussierter arbeite – die vielen anderen Gedanken aus meinem Kopf bringe
  • weil die Lösungen dadurch einfacher und wiederverwendbarer werden
  • weil ich dadurch schneller werde

Ich bin mir 100% sicher, dass ich nicht schneller gewesen wäre, wenn ich nicht-testgetrieben entwickelt hätte.
Weil es komplett illusorisch ist, dass ich noch schneller sein hätte können und z.B. Lukas Stadler (immerhin den Sieger der Catalysts Coding Contests im Jahr 2007 und 2008) überholen hätte können.
Nein, ich bin kein Super-Programmierer, aber mit TDD bin ich dann doch ganz schön gut.
Und am Ende habe ich nicht nur die Lösung, sondern auch das Sicherheitsnetz der kleinen Unit-Tests, die mir ein sorgloses Refactoring der Lösung ermöglichen würden.

Da soll mir einer noch erzählen, dass man mit TDD länger braucht oder langsamer wird – so ein Blödsinn.

Previous Post
Der Name “Catalysts”
Next Post
Raus aus dem Hamsterrad

Related Posts

1 Comment. Leave new

Hi,I’ve found this post and think is very nice and uufesl.can you pls explain me basic steps to implement this solution e.g. in a webpage ? thanks a lot.Max.p.s. in my (few) leasure time I’m trying to create an interface to automate VM snapshot and export them for disaster recovery purposes (actually I’m doing it with a bash script).I’m trying to write it in C#

Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed

Menu