Softwareentwicklung: Testgetriebene Entwicklung nach der St. Pauli Schule

Seite 2: Erkennbare Muster

Inhaltsverzeichnis

Eine Ersetzung der \n-Zeichen durch echte Zeilenumbrüche hebt die Muster hervor:

.A.
B.B
.A.

..A..
.B.B.
C...C
.B.B.
..A..

...A...
..B.B..
.C...C.
D.....D
.C...C.
..B.B..
...A...

Unter anderem fällt auf, dass der Diamant immer aus zwei Hälften (halbe Dreiecke beziehungsweise Pyramiden) besteht und die untere Hälfte ein Spiegelbild der oberen darstellt. Dazwischen gibt es eine Mittellinie, bei der sich der Buchstabe an den äußeren Enden befindet. Beide Muster sind ein Ansatzpunkt für den nächsten Schritt. Collins extrahiert in seinem Beispiel eine Pyramidenfunktion. Dieser Artikel geht stattdessen zunächst dazu über, die Mittellinie zu extrahieren. Beide Ansätze funktionieren gleichermaßen, aber das Extrahieren der Mittellinie vereinfacht den Vergleich mit dem Artikel zur Münchner Schule.

Zunächst gilt es, die Struktur im Code deutlicher sichtbar zu gestalten. Ein Refactoring zerlegt die Strings in Arrays, um sie anschließend mit join wieder zusammenzusetzen. Das Vorgehen ähnelt dem im Beispiel der Münchner Schule, betrifft allerdings mehrere Zweige in der switch-Anweisung.

const diamond = (letter) => {
  switch (letter) {
    case "A":
      return "A";
    case "B": {
      return [".A.", "B.B", ".A."].join("\n");
    }
    case "C": {
      return 
       ["..A..", ".B.B.", "C...C", 
        ".B.B.", "..A.."].join("\n");
    }
  }
};

Wer es erkennt, könnte sofort Tests für eine middleLine-Funktion schreiben. Ansonsten hilft es, zunächst eine Konstante zu extrahieren:

    case "B": {
      const middleLine = "B.B";
      return 
        [".A.", middleLine, ".A."].join("\n");
    }
    case "C": {
      const middleLine = "C...C";
      return 
        ["..A..", ".B.B.", middleLine, 
         ".B.B.", "..A.."].join("\n");
    }

Spätestens jetzt ist klar, dass sich bei den nächsten Schritten das Triangulationsmuster auf niedriger Ebene wiederholt:

describe("middleLine", () => {
  it("should return a middleLine for B", () => {
    expect(middleLine("B")).toEqual("B.B");
  });
});


const middleLine = (letter) => {
  switch (letter) {
    case "B": {
      return "B.B";
    }
  }
};


describe("middleLine", () => {
  it("should return a middleLine for B", () => {
    expect(middleLine("B")).toEqual("B.B");
  });
  it("should return a middleLine for C", () => {
    expect(middleLine("C")).toEqual("C...C");
  });
});


const middleLine = (letter) => {
  switch (letter) {
    case "B": {
      return "B.B";
    }
    case "C": {
      return "C...C";
    }
  }
};

Als nächstes Refactoring lässt sich der Parameter letter einsetzen, um den konstanten String "B" oder "C" zu ersetzen. Ziel ist es dabei immer, die einzelnen Fälle aneinander anzugleichen.

const middleLine = (letter) => {
  switch (letter) {
    case "B": {
      return letter + "." + letter;
    }
    case "C": {
      return letter + "..." + letter;
    }
  }
};

Nun fällt auf, dass eine spaces-Funktion hilfreich wäre, die Leerzeichen beziehungsweise Punkte generiert. Die Umsetzung erfordert lediglich einen TDD-Schritt:

describe("spaces", () => {
  it("should return a given number of spaces", () => {
    expect(spaces(5)).toEqual(".....");
  });
});


const spaces = (n) => ".".repeat(n);

Das Muster dazu heißt Obvious Implementation und stammt ebenfalls von Kent Beck [1]. Die Funktion zeigt den letzten verbliebenen Unterschied deutlich auf:

const middleLine = (letter) => {
  switch (letter) {
    case "B": {
      const numberOfSpacesInMiddleLine = 1;
      return letter + 
      spaces(numberOfSpacesInMiddleLine) + letter;
    }
    case "C": {
      const numberOfSpacesInMiddleLine = 3;
      return letter + 	
        spaces(numberOfSpacesInMiddleLine) + letter;
    }
  }
};

Die Anzahl der Leerzeichen in der Mittellinie ist unterschiedlich. Dabei wiederholt sich der Prozess und führt schließlich zu folgender Implementierung:

describe("numberOfSpacesInMiddleLine", () => {
  it("should return the number of spaces " +  
     "for letter B", () => {
    expect(numberOfSpacesInMiddleLine("B")).toEqual(1);
  });
  it("should return the number of spaces " +
     "for letter C", () => {
    expect(numberOfSpacesInMiddleLine("C")).toEqual(3);
  });
});

 
const numberOfSpacesInMiddleLine = (letter) => {
  switch (letter) {
    case "B":
      return 1;
    case "C":
      return 3;
  }
};

Anschließend stellt sich die Frage, wie sich ein Buchstabe auf die entsprechende Zahl abbilden lässt. Ein Mapping von Buchstaben auf Zahlen hilft:

describe("letterNumber", () => {
  it("should return the number of a letter " + 
     "in the Alphabet", () => {
   expect(letterNumber("C")).toEqual(2);
 });
});

const letterNumber = (letter) => 
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(letter);

In der Mitte steht immer mindestens ein Leerzeichen. Hinzu kommen jeweils zwei für jeden weiteren Buchstaben: 0 bei "B", 2 bei "C", 4 bei "D".

Buchstabe Zahl im Alphabet (0-indiziert)
A 0
B 1
C 2
D 3
E 4
Buchstabe Mittellinie Anzahl Leerzeichen Anzahl Leerzeichen
B B.B 1 1 + 2 * 0 (=B/1 - 1)
C C…C 1 + 2 1 + 2 * 1 (=C/2 - 1)
D D…..D 1 + 4 1 + 2 * 2 (=D/3 - 1)
E E…….E 1 + 6 1 + 2 * 3 (=E/4 - 1)

Das ermöglicht folgendes Refactoring:

const numberOfSpacesInMiddleLine = (letter) => {
  switch (letter) {
    case "B":
      return 1 + 2 * (letterNumber(letter) - 1);
    case "C":
      return 1 + 2 * (letterNumber(letter) - 1);
  }
};

Da beide Zweige identisch sind, darf die switch-Anweisung wegfallen. Damit verallgemeinert sich die Funktion und ist für beliebige Buchstaben verwendbar.

const numberOfSpacesInMiddleLine = (letter) =>
 1 + 2 * (letterNumber(letter) - 1);