Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 7 Min.

Wer errät das Wort?

Das Galgenmännchen war eine gute Übung zum Thema automatisiertes Testen.
Die Übungsaufgabe Galgenmännchen oder Hangman hat mich im zurückliegenden Monat beschäftigt. Das war ein beliebtes Spiel in meiner Schulzeit, einer meiner Lehrer hat es immer vor den Ferien mit uns an der Tafel gespielt.Es geht wie folgt: Ein Wort muss erraten werden, indem man Buchstabe für Buchstabe rät. Dazu wird für jeden Buchstaben des zu erratenden Wortes ein Strich als Platzhalter gezeichnet. Errät man einen Buchstaben, der im Wort vorkommt, wird der Buchstabe an den entsprechenden Stellen eingetragen. Kommt der Buchstabe im Wort nicht vor, wird ein weiterer Strich am Galgenmännchen ergänzt.Ist das Galgenmännchen komplett, hat der Spieler, der das Wort erraten sollte, verloren. Errät er das Wort, bevor das Galgenmännchen vollständig gezeichnet ist, hat er gewonnen.

Los geht’s ...

Natürlich lässt sich ein solches Spiel leicht in Software gießen. Folgendes Listing zeigt das zu realisierende API aus der Aufgabenstellung:

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> Galgenmännchen { 

  <span class="hljs-keyword">public</span> Galgenmännchen(<span class="hljs-built_in">string</span> gesuchtesWort) { ... } 

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">RateBuchstabe</span><span class="hljs-params">(<span class="hljs-keyword">char</span> buchstabe)</span> </span>{ ... } 

} 
 
Dem Konstruktor wird das zu erratende Wort übergeben. Anschließend kann die Methode RateBuchstabe aufgerufen werden, um einen Buchstaben des Wortes zu erraten.Als Ergebnis liefert die Methode einen String zurück. Dieser enthält für jeden Buchstaben des Wortes einen Strich. Alle Buchstaben, die erfolgreich geraten wurden, stehen an den richtigen Stellen im String. Tabelle 1 zeigt einen möglichen Ablauf von API-Aufrufen.

Tabelle 1: Exemplarischer Ablauf von API-Aufrufen

Methode Eingabe Ausgabe
Ctor Developer
RateBuchstabe u ---------
RateBuchstabe e -e-e---e-
RateBuchstabe n -e-e---e-
RateBuchstabe o -e-e-o-e-
RateBuchstabe r -e-e-o-er
RateBuchstabe a -e-e-o-er
RateBuchstabe d De-e-o-er
RateBuchstabe l De-elo-er
RateBuchstabe p De-eloper
RateBuchstabe v Developer
Ich habe meine Implementation testgetrieben begonnen. Dazu habe ich zunächst folgenden Test erstellt:

<span class="hljs-keyword">using</span> NUnit.Framework; 

<span class="hljs-keyword">namespace</span> <span class="hljs-title">hangman.logic.tests</span> 
{ 
  [TestFixture] 
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HangmanTests</span> 
  { 
    [Test] 
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Wrong_guess_after_ctor</span>(<span class="hljs-params"></span>) </span>{ 
      <span class="hljs-keyword">var</span> sut = <span class="hljs-keyword">new</span> Hangman(<span class="hljs-string">"Developer"</span>); 
      Assert.That(sut.Guess(<span class="hljs-string">'u'</span>), 
        Is.EqualTo(<span class="hljs-string">"---------"</span>)); 
    } 
  } 
} 
 
Sicherlich ist Ihnen aufgefallen, dass ich das API mal eben ins Englische übersetzt habe. Das lag daran, dass ich mit der Lösung begonnen habe, ohne mir die Aufgabenstellung noch einmal im Detail anzusehen. In meinen Clean-Code-Develo­per-Workshops sind solche Übersetzungen von APIs an der Tagesordnung.Viele Entwickler sind es gewohnt, die Domänensprache vollständig ins Englische zu übertragen. Häufig kommt auch der Einwand, dass deutsche Begriffe in Kombination mit den englischen Schlüsselwörter der Sprache schwerer zu lesen seien, was für eine Übersetzung spricht. So gibt es viele Argumente für und gegen deutsche oder englische Begriffe. Letztlich müssen Sie sich im Team festlegen. Und existiert erst einmal eine ausreichend große Codebasis, werden Sie die Sprache sicherlich nicht mehr wechseln.Doch zurück zum Galgenmännchen. Um den ersten Test grün zu bekommen, war nicht sonderlich viel Implementa­tion erforderlich:

<span class="hljs-keyword">namespace</span> <span class="hljs-title">hangman.logic</span> 
{ 
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Hangman</span> 
  { 
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">string</span> _word; 

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Hangman</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> word</span>) </span>{ 
      _word = word; 
    } 

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> <span class="hljs-title">Guess</span>(<span class="hljs-params"><span class="hljs-keyword">char</span> ch</span>) </span>{ 
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">string</span>(<span class="hljs-string">'-'</span>, _word.Length); 
    } 
  } 
} 
 
Das im Konstruktor übergebene Wort wird in einem Feld der Klasse abgelegt. So kann in der Methode Guess darauf zugegriffen werden, um die Anzahl der auszugebenden Striche zu ermitteln.Bis hierher war es noch einfach. Mein nächster Test sah dann wie folgt aus:

[Test] 
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Correct_guess_after_ctor</span>(<span class="hljs-params"></span>) </span>{ 
  <span class="hljs-keyword">var</span> sut = <span class="hljs-keyword">new</span> Hangman(<span class="hljs-string">"Developer"</span>); 
  Assert.That(sut.Guess(<span class="hljs-string">'e'</span>), Is.EqualTo(<span class="hljs-string">"-e-e---e-"</span>)); 
} 
 
Jetzt müssen die tatsächlich vorhandenen Buchstaben, hier „e“, im Wort gefunden und ausgegeben werden. Die Implementation dazu ist ebenfalls einfach, wie Sie in Listing 1 erkennen können.
Listing 1: Buchstaben finden und ausgeben
&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; &lt;span class="hljs-title"&gt;hangman.logic&lt;/span&gt; &lt;br/&gt;{ &lt;br/&gt;  &lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;Hangman&lt;/span&gt; &lt;br/&gt;  { &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;readonly&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt; _word; &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;readonly&lt;/span&gt; &lt;span class="hljs-keyword"&gt;char&lt;/span&gt;[] _result; &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-title"&gt;Hangman&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;string&lt;/span&gt; word&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      _word = word; &lt;br/&gt;      _result = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;(&lt;br/&gt;        &lt;span class="hljs-string"&gt;'-'&lt;/span&gt;, _word.Length).ToCharArray(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt; &lt;span class="hljs-title"&gt;Guess&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;char&lt;/span&gt; ch&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;var&lt;/span&gt; i = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; i &amp;lt; _word.Length; i++) { &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_word[i] == ch) { &lt;br/&gt;          _result[i] = ch; &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;(_result); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;}  
Nun wird im Konstruktor das Feld _result mit Strichen vorbelegt. Ich habe hier ein Character-Array gewählt, da ein String immutable ist. In der Guess-Methode wird nun jedes Zeichen, das im Wort an der entsprechenden Stelle vorkommt, in das Feld _result übernommen. Erst bei der Rückgabe des Ergebnisses wird dann aus dem char[] ein String erzeugt.So simpel kann die Lösung einer einfachen Aufgabenstellung aussehen. Doch wie sieht es mit den anderen Testfällen aus? Schließlich enthält die Aufgabenstellung eine Tabelle mit Testdaten. Die sollten unbedingt in automatisierte Tests überführt werden. Im realen Projekt schafft dies Vertrauen zwischen Team und Product Owner. Den zugehörigen Test zeigt Listing 2.
Listing 2: Test der Beispieldaten
[Test] &lt;br/&gt;public void Integrationstest() { &lt;br/&gt;  var sut = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Hangman(&lt;span class="hljs-string"&gt;"Developer"&lt;/span&gt;); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'u'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"---------"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'e'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"-e-e---e-"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'n'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"-e-e---e-"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'o'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"-e-e-o-e-"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'r'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"-e-e-o-er"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'a'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"-e-e-o-er"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'d'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"De-e-o-er"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'l'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"De-elo-er"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'p'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"De-eloper"&lt;/span&gt;)); &lt;br/&gt;  Assert.That(&lt;br/&gt;    sut.Guess(&lt;span class="hljs-string"&gt;'v'&lt;/span&gt;), &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.EqualTo(&lt;span class="hljs-string"&gt;"Developer"&lt;/span&gt;)); &lt;br/&gt;}  
Und siehe da: Rot! Leider hatte ich nämlich übersehen, dass der Anfangsbuchstabe zwar als kleiner Buchstabe geraten wird, dann aber natürlich als Großbuchstabe im Ergebnis erscheinen soll.An genau dieser Stelle schlägt der Test allerdings fehl. Die Implementation musste also noch einmal korrigiert werden.Um den Testfall zu fokussieren, habe ich den folgenden Test ergänzt:

[Test] 
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Capital_letters_are_found</span>(<span class="hljs-params"></span>) </span>{ 
  <span class="hljs-keyword">var</span> sut = <span class="hljs-keyword">new</span> Hangman(<span class="hljs-string">"dDaA"</span>); 
  Assert.That(sut.Guess(<span class="hljs-string">'d'</span>), Is.EqualTo(<span class="hljs-string">"dD--"</span>)); 
  Assert.That(sut.Guess(<span class="hljs-string">'A'</span>), Is.EqualTo(<span class="hljs-string">"dDaA"</span>)); 
} 
 
Dieser Test stellt sicher, dass die Groß-/Kleinschreibung nicht relevant ist. Buchstaben können in beliebiger Schreibweise geraten werden und werden dann trotzdem in der Schreibweise, in der sie in das zu erratende Wort eingestellt sind, übernommen.Die korrigierte Implementation sieht wie folgt aus:
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> <span class="hljs-title">Guess</span>(<span class="hljs-params"><span class="hljs-keyword">char</span> ch</span>) </span>{ 
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i &lt; _word.Length; i++) { 
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">char</span>.ToLower(_word[i]) == <span class="hljs-keyword">char</span>.ToLower(ch)) { 
      _result[i] = _word[i]; 
    } 
  } 
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">string</span>(_result); 
} 
 
Nun läuft alles so, wie es soll: Die Buchstaben werden vor dem Vergleich beide in Kleinbuchstaben umgewandelt. Außerdem wird nicht das übergebene Zeichen ch in das Ergebnis übernommen, sondern der Buchstabe aus dem Wort. Jetzt sind auch alle Tests grün.

Zwischenfazit

Selbst eine so einfache Aufgabenstellung bietet Raum für Fehler. Beim flüchtigen Beantworten der Frage „Bin ich fertig?“ habe ich nämlich viel zu schnell „Ja“ gedacht. Mir war zunächst überhaupt nicht aufgefallen, dass es ein Problem geben könnte. Erst das Ergänzen der weiteren Testfälle aus den Anforderungen hat das zutage gefördert. Es gilt also immer: Sorgfältig arbeiten!

Mehr!

Nun hatte ich den Wunsch, das API noch etwas zu erweitern. Man kann beispielsweise nicht erkennen, ob das Wort korrekt geraten wurde.Dazu müsste man selbst feststellen, ob noch wenigstens ein Strich im Ergebnis vorhanden ist. Besser wäre eine boolesche Eigenschaft. Also habe ich einen Test geschrieben, der ausdrückt, wie sich die Ergänzung verhalten soll. Den Test sehen Sie in Listing 3.
Listing 3: Test der Ergänzung
[Test] &lt;br/&gt;public void Guessed_is_false_after_ctor() { &lt;br/&gt;  var sut = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Hangman(&lt;span class="hljs-string"&gt;"a"&lt;/span&gt;); &lt;br/&gt;  Assert.That(sut.Guessed, &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.&lt;span class="hljs-literal"&gt;False&lt;/span&gt;); &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;[Test] &lt;br/&gt;public void Guessed_is_true_after_word_is_guessed() { &lt;br/&gt;  var sut = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Hangman(&lt;span class="hljs-string"&gt;"Aber"&lt;/span&gt;); &lt;br/&gt;  sut.Guess(&lt;span class="hljs-string"&gt;'a'&lt;/span&gt;); &lt;br/&gt;  Assert.That(sut.Guessed, &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.&lt;span class="hljs-literal"&gt;False&lt;/span&gt;); &lt;br/&gt;  sut.Guess(&lt;span class="hljs-string"&gt;'r'&lt;/span&gt;); &lt;br/&gt;  Assert.That(sut.Guessed, &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.&lt;span class="hljs-literal"&gt;False&lt;/span&gt;); &lt;br/&gt;  sut.Guess(&lt;span class="hljs-string"&gt;'b'&lt;/span&gt;); &lt;br/&gt;  Assert.That(sut.Guessed, &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.&lt;span class="hljs-literal"&gt;False&lt;/span&gt;); &lt;br/&gt;  sut.Guess(&lt;span class="hljs-string"&gt;'e'&lt;/span&gt;); &lt;br/&gt;  Assert.That(sut.Guessed, &lt;span class="hljs-keyword"&gt;Is&lt;/span&gt;.&lt;span class="hljs-literal"&gt;True&lt;/span&gt;); &lt;br/&gt;}  
Die zugehörige Implementation einer Property ist trivial:

<span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> Guessed =&gt; !_result.Contains(<span class="hljs-string">'-'</span>); 
 
Doch nun hat die Implementation ein unschönes Verhalten: Der Wert der Property hängt davon ab, dass ein Bindestrich im Ergebnis verwendet wird. Was aber passiert, wenn der Konstruktor mit einer Zeichenkette aufgerufen wird, die einen solchen Bindestrich enthält? Des Weiteren ist der Bindestrich nun an zwei Stellen über den Code verteilt.„Don’t Repeat Yourself“ lautet die ewige Mahnung an den Clean Code Developer. Also habe ich den Code noch etwas refaktorisiert. Zum einen habe ich für den Bindestrich eine Konstante eingeführt. Zum anderen wird nun im Konstruktor eine Ausnahme ausgelöst, wenn das zu erratende Wort den Bindestrich enthält.Der zugehörige Test:

[Test] 
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> </span>
<span class="hljs-function">  <span class="hljs-title">Exception_is_thrown_if_word_contains_hyphen</span>(<span class="hljs-params"></span>) </span>{ 
  <span class="hljs-keyword">var</span> e = Assert.Throws&lt;ArgumentException&gt;(() =&gt; 
    <span class="hljs-keyword">new</span> Hangman(<span class="hljs-string">"-"</span>)); 
  Assert.That(e.Message, Is.EqualTo(
    <span class="hljs-string">"Word must not contain hyphen '-'."</span>)); 
} 
 
Wie die modifizierte Implementation aussieht, können Sie anhand von Listing 4 nachvollziehen.
Listing 4: Die Implementation, ergänzt um die Behandlung des Bindestrichs
&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; System; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; System.Linq; &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; &lt;span class="hljs-title"&gt;hangman.logic&lt;/span&gt; &lt;br/&gt;{ &lt;br/&gt;  &lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;Hangman&lt;/span&gt; &lt;br/&gt;  { &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; &lt;span class="hljs-keyword"&gt;char&lt;/span&gt; Hyphen = &lt;span class="hljs-string"&gt;'-'&lt;/span&gt;; &lt;br/&gt;    &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;readonly&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt; _word; &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;readonly&lt;/span&gt; &lt;span class="hljs-keyword"&gt;char&lt;/span&gt;[] _result; &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-title"&gt;Hangman&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;string&lt;/span&gt; word&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (word.Contains(Hyphen)) { &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;throw&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; ArgumentException(&lt;br/&gt;          &lt;span class="hljs-string"&gt;$"Word must not contain hyphen &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-string"&gt;          '&lt;span class="hljs-subst"&gt;{Hyphen}&lt;/span&gt;'."&lt;/span&gt;); &lt;br/&gt;      } &lt;br/&gt;      _word = word; &lt;br/&gt;      _result = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;(&lt;br/&gt;        Hyphen, _word.Length).ToCharArray(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt; &lt;span class="hljs-title"&gt;Guess&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;char&lt;/span&gt; ch&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;var&lt;/span&gt; i = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; i &amp;lt; _word.Length; i++) { &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;char&lt;/span&gt;.ToLower(_word[i]) == &lt;br/&gt;          &lt;span class="hljs-keyword"&gt;char&lt;/span&gt;.ToLower(ch)) { &lt;br/&gt;          _result[i] = _word[i]; &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;(_result); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;bool&lt;/span&gt; Guessed =&amp;gt; !_result.Contains(Hyphen); &lt;br/&gt;  } &lt;br/&gt;}  
Nun ist die Implementation doch schon etwas länger geraten als in der ursprünglichen Variante. Und dennoch fehlt ­immer noch eine wichtige Eigenschaft: Nach jedem Raten sollte dem Aufrufer mitgeteilt werden, ob der Versuch erfolgreich war oder ob ein weiterer Strich am Galgenmännchen ergänzt werden muss.Um das zu erreichen, müsste die Guess-Methode entweder zwei Werte zurückliefern, oder aber es wird eine weitere Eigenschaft ergänzt.Zwar sind dank der Tupel-Notation mehrere Rückgabewerte in C# kein Problem mehr, doch ich halte hier eine andere Lösung für eleganter. Die Guess-Methode liefert zukünftig einen bool zurück. Dieser ist true, wenn der Buchstabe im Wort vorkommt, andernfalls false. Diese Änderung hat allerdings den gravierenden Nachteil, dass mir damit einige Tests syntaktisch kaputtgehen.An dieser Stelle kann man erkennen, dass das Erstellen eines API nicht trivial ist. Ein API „auf der grünen Wiese“ zu entwerfen ist selten eine gute Idee. In der Regel übersieht man dabei Details. Diese tauchen auf, sobald man beginnt, das API in einer realen Anwendung zu verwenden.Ich habe die Änderungen schrittweise vorgenommen. Zuerst habe ich die Eigenschaft Result ergänzt. Anschließend habe ich dann die Tests so modifiziert, dass nach jedem Aufruf der Guess-Methode die Result-Eigenschaft überprüft wird. Folgender Test zeigt dies exemplarisch:

[Test] 
public void Wrong_guess_after_ctor() { 
  var sut = new Hangman("Developer"); 
  Assert.That(sut.Guess('u'), Is.EqualTo("---------")); 
  Assert.That(sut.Result, Is.EqualTo("---------")); 
} 
 
Die Result-Eigenschaft ist wie folgt definiert:

public string Result =&gt; new string(_result); 
 
Anschließend habe ich die Signatur der Methode Guess so geändert, dass sie einen bool liefert anstelle des Strings. Das führte natürlich dazu, dass ich die Tests anpassen musste. Diese Änderung ging allerdings leicht von der Hand, es war lediglich Fleißarbeit (Listing 5).
Listing 5: Die angepassten Tests nach Änderung von Guess
[Test] &lt;br/&gt;public void Integrationstest() { &lt;br/&gt;  var sut = new Hangman("Developer"); &lt;br/&gt;  Assert.That(sut.Guess('u'), Is.False); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("---------")); &lt;br/&gt;  Assert.That(sut.Guess('e'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("-e-e---e-")); &lt;br/&gt;  Assert.That(sut.Guess('n'), Is.False); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("-e-e---e-")); &lt;br/&gt;  Assert.That(sut.Guess('o'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("-e-e-o-e-")); &lt;br/&gt;  Assert.That(sut.Guess('r'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("-e-e-o-er")); &lt;br/&gt;  Assert.That(sut.Guess('a'), Is.False); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("-e-e-o-er")); &lt;br/&gt;  Assert.That(sut.Guess('d'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("De-e-o-er")); &lt;br/&gt;  Assert.That(sut.Guess('l'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("De-elo-er")); &lt;br/&gt;  Assert.That(sut.Guess('p'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("De-eloper")); &lt;br/&gt;  Assert.That(sut.Guess('v'), Is.True); &lt;br/&gt;  Assert.That(sut.Result, Is.EqualTo("Developer")); &lt;br/&gt;}  
Die vollständige Implementation sieht damit aus wie in Listing 6 gezeigt.
Listing 6: Die Implementation, jetzt komplett
using System; &lt;br/&gt;using System.Linq; &lt;br/&gt;&lt;br/&gt;namespace hangman.logic &lt;br/&gt;{ &lt;br/&gt;  public class Hangman &lt;br/&gt;  { &lt;br/&gt;    private const char Hyphen = '-'; &lt;br/&gt;    &lt;br/&gt;    private readonly string _word; &lt;br/&gt;    private readonly char[] _result; &lt;br/&gt;&lt;br/&gt;    public Hangman(string word) { &lt;br/&gt;      if (word.Contains(Hyphen)) { &lt;br/&gt;        throw new ArgumentException(&lt;br/&gt;        $"Word must not contain hyphen '{Hyphen}'."); &lt;br/&gt;      } &lt;br/&gt;      _word = word; &lt;br/&gt;      _result = new string(&lt;br/&gt;        Hyphen, _word.Length).ToCharArray(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public bool Guess(char ch) { &lt;br/&gt;      var guessedCorrect = false; &lt;br/&gt;      for (var i = 0; i &amp;lt; _word.Length; i++) { &lt;br/&gt;        if (char.ToLower(_word[i]) == &lt;br/&gt;          char.ToLower(ch)) { &lt;br/&gt;          _result[i] = _word[i]; &lt;br/&gt;          guessedCorrect = true; &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;      return guessedCorrect; &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public string Result =&amp;gt; new string(_result); &lt;br/&gt;    &lt;br/&gt;    public bool Guessed =&amp;gt; !_result.Contains(Hyphen); &lt;br/&gt;  } &lt;br/&gt;}  
Mit dieser Implementation und den zugehörigen Tests gebe ich mich nun zufrieden.

Fazit

Es fällt schwer, ein gutes API praktisch „aus der hohlen Hand“ zu ­erstellen. Ohne einen konkreten Anwendungsfall ist es nicht ganz einfach, die Methoden und Eigenschaften so zu wählen, dass sie später auch tatsächlich leicht verwendet werden können.Ob mir dies gelungen ist, steht letztlich nicht fest, da ich die Klasse Hangman bislang nur in den Tests verwende. Ein Nachweis würde erst durch Integration der Klasse in ein Programm erbracht. Das wäre doch eine prima Übung für den vor Ihnen liegenden Monat!

In eigener Sache

Ich arbeite derzeit an einem Buch über Flow Design, die ­Entwurfsmethode, die Sie in der dotnetpro in zahlreichen meiner dojoLösungen in Aktion sehen konnten.Damit ich die abschließenden Arbeiten konzentriert und fokussiert voranbringen kann, werden Sie einige Monate auf Übungsaufgabe und Lösung verzichten müssen.Zum Jahresende bin ich wieder da – versprochen!
Projektdateien herunterladen

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige