Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 11 Min.

Kniffliges Kniffel

Spiele stellen den Entwickler immer wieder vor spannende Herausforderungen. In diesem Fall ist es ein Würfelspiel: Kniffel, auch bekannt als Yazee.
In diesem Monat ging es darum, das Würfelspiel Kniffel [1] zu implementieren. Die Aufgabe ist vermutlich zu umfangreich, um sie vollständig innerhalb der für Entwickler empfohlenen Übungszeit zu lösen. Wir empfehlen unseren Workshop-Teilnehmern, zwei Stunden pro Woche ins Üben zu investieren [2]. Bei der monatlichen Erscheinungsweise der dotnetpro ergibt sich damit eine Übungszeit von acht Stunden für die jeweilige Aufgabe. Sicherlich knapp bemessen, um ein vollständiges Kniffelspiel zu implementieren. Für mich, und vielleicht auch für Sie, bestand somit die erste Herausforderung darin, einen ausreichend kleinen Ausschnitt der Anforderungen auszuwählen, um mit diesem zu beginnen.Und ich kann gar nicht oft genug betonen, dass schon allein diese Übung nicht oft genug praktiziert werden kann: Anforderungen so weit zu zerlegen, bis schließlich ein „Häppchen“ übrig bleibt, das in vier bis acht Stunden umgesetzt ist. Üben Sie lieber, die Anforderungen noch kleiner zu zerlegen. Zu klein gibt es eigentlich nicht, solange Sie vertikal schneiden, also jeweils einen Durchstich, ein Inkrement, produzieren.

Bottom-up vs. top-down

Ich habe bei dieser Aufgabe mit der Frage experimentiert, was passiert, wenn ich bottom-up beginne statt top-down. Die Empfehlung für Softwareentwicklungsprozesse lautet immer wieder, top-down vorzugehen, also bei konkreten Anforderungen des Product Owners zu beginnen. Bottom-up würde bedeuten, mit Funktionalität „von unten“ zu beginnen, von der wir vermuten, dass sie benötigt wird. An der Formulierung merken Sie vielleicht schon, welches Problem sich bei der Bottom-up-Vorgehensweise ergibt: Das wichtige Wort war „vermuten“.Wenn wir top-down vorgehen, ergibt sich der benötigte Funktionsumfang einer Funktionseinheit (Methode, Klasse, Bibliothek, Komponente, Microservice) von oben nach unten aus den Anforderungen des Product Owners. Bei sorgfältiger Herangehensweise ist sichergestellt, dass genau die Funk­tionalität implementiert wird, die sich zuvor aus dem Entwurf ergeben hat, der sich wiederum aus den Anforderungen ergeben hat. Arbeite ich dagegen bottom-up, ist die Wahrscheinlichkeit groß, dass der vermutete(!) Funktionsumfang nicht passgenau den Anforderungen entspricht.Dazu ein Beispiel: Eine Anwendung soll realisiert werden, die ihre Daten persistieren muss, damit diese vom einen zum nächsten Programmlauf erhalten bleiben. Wenn Sie nun nicht von den Anforderungen ausgehen und planen, welchen Funktionsumfang Sie von einer Persistenzklasse erwarten, bleibt Ihnen eigentlich nichts anderes übrig, als CRUD zu implementieren. CRUD steht für Create, Retrieve, Update und Delete. Und da zu diesem Zeitpunkt auch keine Datentypen klar entworfen sind, läuft es vielleicht sogar auf einen generischen Persistenzmechanismus hinaus.Endlich mal wieder Infrastruktur basteln, wird mancher Entwickler vielleicht denken. Tatsächlich verschwendet er jedoch sehr wahrscheinlich etwas Zeit, da Funktionalität erstellt wird, die niemand benötigt. Kommen Sie dagegen von „oben“, von den Anforderungen, ergibt sich durch den Entwurf eine klare Schnittstelle für die Persistenz. Vielleicht stellen Sie auf diese Weise fest, dass Delete gar nicht benötigt wird, weil Datensätze stattdessen als „gelöscht“ markiert werden, aber erhalten bleiben. Wir halten also fest: Top-down, von den Anforderungen über den Entwurf zum Code, ist effizienter als bottom-up.

Kniffel bottom-up

Mein Versuch, das Kniffelspiel trotzdem, wenigstens in Teilen, bottom-up zu realisieren, führte dann tatsächlich zu Mehrarbeit. Ich hatte mir nämlich eine Klasse Würfelbecher ausgedacht, die fünf Würfel per Zufallszahlengenerator würfeln kann. Das Besondere: Man konnte null bis fünf Würfel zurück­legen und dann erneut würfeln. Im realen Spiel macht man das tatsächlich: Mit jedem Wurf kann man entscheiden, welche Würfel liegen bleiben sollen und welche man neu würfeln möchte.Bei meiner Implementation stellte sich jedoch heraus, dass meine Vorgehensweise eine Vermischung von Aspekten darstellte und die Sache zudem noch verkompliziert hat. Also habe ich meine ursprüngliche Implementation mitsamt der Unit-Tests modifiziert. Mehraufwand, der nicht angefallen wäre, hätte ich konsequent anforderungsgetrieben entworfen und dann implementiert. Eine wichtige Erkenntnis!

Jetzt mal ordentlich

Mein nächster Versuch bestand daher darin, wieder „ordentlich“ vorzugehen. Ich habe also mit den Anforderungen begonnen und diese in Dialog und Interaktionen zerlegt. Bild 1 zeigt das Ergebnis meiner Analyse.
Ich habe ein Konsolen-UI entworfen und an dieses die Interaktionen als Pfeile gezeichnet. Für ein Konsolen-UI habe ich mich entschieden, weil es schneller zu implementieren ist als ein GUI. Der Anwender kann in diesem Dialog folgende Interaktionen auslösen:
  • Würfeln: Bis zu fünf Würfel werden gewürfelt. Das Ergebnis wird unterhalb der Tabelle angezeigt. Jeder Würfel ist in eckigen Klammern dargestellt.
  • Würfel umschalten: Mit den Tasten 1 bis 5 kann der Benutzer entscheiden, ob ein Würfel auf dem Tisch liegen bleiben oder in den Würfelbecher für den nächsten Wurf zurückgelegt werden soll.
  • Setzen: Der Anwender kann die fünf Würfel in die Tabelle übernehmen. Es erfolgt dann eine Abfrage der Koordinaten.
Bei der Suche nach Interaktionen sind jeweils das Starten und Beenden der Anwendung zu prüfen. In diesem Fall beginnt die Anwendung beim Start mit einem leeren Spielzettel. Damit ist Start als Interaktion relevant, weil das leere Spiel initialisiert werden muss. Ferner müssen initial ein leerer Spielzettel sowie der erste Spieler angezeigt werden. Wenn man die Anwendung verlässt, geht der Spielstand verloren.
Beenden ist daher als Interaktion nicht relevant, weil lediglich die Anwendung beendet wird. Bei einer Konsolenanwendung muss dazu im technischen Sinne Logik implementiert werden. Allerdings handelt es sich dabei nicht um Domänenlogik, da es beim Beenden nicht speziell um das Thema dieser Anwendung geht. Die Domänenlogik ist es aber, die eine Interaktion begründet. Sollten sich die Anforderungen im Hinblick auf das Beenden der Anwendung ändern, kommt die Interaktion Beenden möglicherweise hinzu.Nachdem ich die Interaktionen identifiziert hatte, habe
ich die Entwürfe auf der obersten Ebene erstellt. Bild 2 zeigt das Ergebnis.Zum Würfeln betätigt der Anwender die Taste W. Das UI löst daraufhin einen Event aus, der keine Daten trägt. Der Event landet bei der Funktionseinheit Würfeln. Diese ist dafür zuständig, beim ersten Wurf des Spiels fünf Würfel zu würfeln. Das Ergebnis sind dann zwei Aufzählungen:
  • tisch*: Eine Liste der Würfel, die auf dem Tisch liegen. Initial ist diese Liste leer.
  • becher*: Eine Liste von Würfeln, die gerade gefallen sind. Initial enthält diese Liste nach dem Würfeln fünf Würfel.
Mit der Interaktion Würfel umschalten kann der Anwender nun entscheiden, welche Würfel er vom Becher auf den Tisch platzieren möchte. Betätigt er die Taste 1, wird der erste Würfel vom Becher auf den Tisch gelegt. Betätigt er nochmals die 1, wird der Würfel wieder zurückgelegt.Anschließend kann der Benutzer entweder erneut würfeln, sofern er seine drei Würfe noch nicht ausgeschöpft hat, oder mit S wie Setzen alle fünf Würfel in die Tabelle übertragen. Dazu fragt das UI die Koordinate ab, in die der Wurf übertragen werden soll. Die Spalten sind dazu von A bis F überschrieben, die Zeilen von 1 bis 15 durchnummeriert.
Listing 1: Die Klasse Program
<span class="hljs-keyword">using</span> System; <br/><span class="hljs-keyword">using</span> kniffel.contracts; <br/><span class="hljs-keyword">using</span> kniffel.ui; <br/><br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel</span> <br/>{ <br/>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Program</span> <br/>  { <br/>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-keyword">string</span>[] args</span>) </span>{ <br/>      <span class="hljs-keyword">var</span> consoleUi = <span class="hljs-keyword">new</span> ConsoleUi(); <br/>      <span class="hljs-keyword">var</span> interactors = <span class="hljs-keyword">new</span> Interactors(); <br/><br/><br/>      consoleUi.Würfeln += () => { <br/>        <span class="hljs-keyword">int</span>[] drinnen; <br/>        <span class="hljs-keyword">int</span>[] draußen; <br/>        interactors.Würfeln(<span class="hljs-keyword">out</span> drinnen, <span class="hljs-keyword">out</span> draußen); <br/>        consoleUi.Wurf_anzeigen(drinnen, draußen); <br/>      }; <br/><br/>      consoleUi.Würfel_umschalten += i => { <br/>        <span class="hljs-keyword">int</span>[] drinnen; <br/>        <span class="hljs-keyword">int</span>[] draußen; <br/>        interactors.Würfel_umschalten(<br/>          i, <span class="hljs-keyword">out</span> drinnen, <span class="hljs-keyword">out</span> draußen); <br/>        consoleUi.Wurf_anzeigen(drinnen, draußen); <br/>      }; <br/><br/>      consoleUi.Setzen += koordinate => { <br/>        Tabelle tabelle; <br/>        <span class="hljs-keyword">int</span> spieler; <br/>        interactors.Setzen(<br/>          koordinate, <span class="hljs-keyword">out</span> tabelle, <span class="hljs-keyword">out</span> spieler); <br/>        consoleUi.Spielstand_anzeigen(<br/>          tabelle, spieler); <br/>      }; <br/><br/>      Action start = () => { <br/>        Tabelle tabelle; <br/>        <span class="hljs-keyword">int</span> spieler; <br/>        interactors.Start(<span class="hljs-keyword">out</span> tabelle, <span class="hljs-keyword">out</span> spieler); <br/>        consoleUi.Spielstand_anzeigen(<br/>          tabelle, spieler); <br/>        consoleUi.MessageLoop(); <br/>      }; <br/><br/>      start(); <br/>    } <br/>  } <br/>} <br/><br/> 
Kann der Wurf nicht in das angegebene Feld übernommen werden, muss der Benutzer erneut das Setzen auslösen. Dies ist der Fall, wenn das Feld bereits verwendet wurde oder wenn der Wurf aufgrund der Regeln nicht in das Feld übernommen werden kann. Konnte das Setzen durchgeführt ­werden, wird der Spieler gewechselt. Die Interaktion Setzen liefert dazu neben der Tabelle auch einen String, der den Spieler identifiziert.

Top-down

Mit der Umsetzung des Entwurfs bin ich von oben nach unten vorgegangen. Listing 1 zeigt die Klasse Program, die mit Main den Einstiegspunkt in die Anwendung enthält.In der Main-Methode wird jeweils das Ui mit dem Inter­actor verbunden. Mit Interactor bezeichnen wir eine Me­thode, die eine Interaktion realisiert. Die benötigten Daten­typen zeigt folgendes Listing:
<span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel.contracts</span> 
{ 
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Tabelle</span> 
  { 
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Tabelle</span>(<span class="hljs-params"></span>) </span>{ 
      Zeilen = <span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">15</span>][]; 
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">15</span>; i++) { 
        Zeilen[i] = <span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">6</span>]; 
      } 
    } 
 
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span>[][] Zeilen { <span class="hljs-keyword">get</span>; } 
  } 
} 
 
 
<span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel.contracts</span> 
{ 
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">struct</span> Koordinate 
  { 
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Koordinate</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> spalte, <span class="hljs-keyword">int</span> zeile</span>) </span>{ 
      Spalte = spalte; 
      Zeile = zeile; 
    } 
 
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Spalte { <span class="hljs-keyword">get</span>; } 
     <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Zeile { <span class="hljs-keyword">get</span>; } 
  } 
} 
Die Klasse ConsoleUi realisiert die Ein-/Ausgabe. Sie ist vom Umfang her etwas länger geraten, enthält aber lediglich trivialen Code zur Formatierung und Ausgabe der Werte und fällt in die Kategorie „Fleißarbeit“.In der Klasse Interactors sind die Methoden zusammengefasst, die jeweils eine Interaktion realisieren.
Listing 2: Die Klasse Interactors
&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; kniffel.contracts; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; kniffel.game; &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; &lt;span class="hljs-title"&gt;kniffel&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;Interactors&lt;/span&gt; &lt;br/&gt;  { &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; Kniffel kniffel; &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;void&lt;/span&gt; &lt;span class="hljs-title"&gt;Start&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;out&lt;/span&gt; Tabelle tabelle, &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;      &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; spieler&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      kniffel = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Kniffel(); &lt;br/&gt;      kniffel.Start(&lt;span class="hljs-keyword"&gt;out&lt;/span&gt; tabelle, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; spieler); &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;void&lt;/span&gt; Würfeln(&lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] becher, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] &lt;br/&gt;      tisch) { &lt;br/&gt;      kniffel.Würfeln(&lt;span class="hljs-keyword"&gt;out&lt;/span&gt; becher, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; tisch);            &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;void&lt;/span&gt; Würfel_umschalten(&lt;br/&gt;      &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; i, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] becher, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] tisch) { &lt;br/&gt;      kniffel.Würfel_umschalten(&lt;br/&gt;        i, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; becher, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; tisch);            &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;void&lt;/span&gt; &lt;span class="hljs-title"&gt;Setzen&lt;/span&gt;(&lt;span class="hljs-params"&gt;Koordinate koordinate, &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;      &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; Tabelle tabelle, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; spieler&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      kniffel.Setzen(koordinate, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; tabelle, &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; spieler); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt; 
Zurzeit wird hier lediglich die Klasse Kniffel integriert, in der die Domänenlogik realisiert ist. Ich rechne damit, dass später weitere Klassen zu integrieren sind, daher habe ich die Klasse Interactors angelegt. Ferner ergibt sich so eine Struktur, die über viele Anwendungen und Beispiele hinweg immer wieder die gleiche ist. So wird es leichter, sich in unterschiedlichen Projekten zurechtzufinden, da sich die wichtigsten Strukturelemente in jedem Projekt wiederholen.

Logik

Die Domänenlogik des Spiels ist in der Klasse Kniffel realisiert; sie ist in Listing 3 zu sehen.Der Funktionsumfang der Anwendung ist bislang unvollständig. Man kann würfeln und die Würfel zwischen Becher und Tisch hin und her wechseln. Ferner kann man den Wurf in die Tabelle übernehmen. Allerdings wird dabei lediglich die Summe der fünf Würfel berechnet und übernommen.
Listing 3: Die Klasse Kniffel
&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; System.Linq; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; kniffel.contracts; &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; &lt;span class="hljs-title"&gt;kniffel.game&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;Kniffel&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;int&lt;/span&gt;[] _becher = { &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt; }; &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] _tisch = { &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt; }; &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; _wurf; &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; Würfelbecher würfelbecher = &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Würfelbecher(); &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; Tabelle[] _tabelle; &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; _spieler; &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;void&lt;/span&gt; &lt;span class="hljs-title"&gt;Start&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;out&lt;/span&gt; Tabelle tabelle, &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;      &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; spieler&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      _tabelle = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Tabelle[&lt;span class="hljs-number"&gt;2&lt;/span&gt;]; &lt;br/&gt;      _tabelle[&lt;span class="hljs-number"&gt;0&lt;/span&gt;] = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Tabelle(); &lt;br/&gt;      _tabelle[&lt;span class="hljs-number"&gt;1&lt;/span&gt;] = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Tabelle(); &lt;br/&gt;      _spieler = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; &lt;br/&gt;      _wurf = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; &lt;br/&gt;&lt;br/&gt;      tabelle = _tabelle[_spieler]; &lt;br/&gt;      spieler = _spieler + &lt;span class="hljs-number"&gt;1&lt;/span&gt;; &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;void&lt;/span&gt; Würfeln(&lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] becher, &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] tisch) { &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_wurf == &lt;span class="hljs-number"&gt;3&lt;/span&gt;) { &lt;br/&gt;         becher = _becher; &lt;br/&gt;         tisch = _tisch; &lt;br/&gt;         &lt;span class="hljs-keyword"&gt;return&lt;/span&gt;; &lt;br/&gt;      } &lt;br/&gt;      _wurf += &lt;span class="hljs-number"&gt;1&lt;/span&gt;; &lt;br/&gt;&lt;br/&gt;      &lt;span class="hljs-keyword"&gt;var&lt;/span&gt; anzahl_Würfel = _becher.Count(i =&amp;gt; i != &lt;span class="hljs-number"&gt;0&lt;/span&gt;); &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;var&lt;/span&gt; wurf = würfelbecher.Würfeln(anzahl_Würfel); &lt;br/&gt;&lt;br/&gt;      Würfel_übernehmen(wurf, _becher); &lt;br/&gt;&lt;br/&gt;      becher = _becher; &lt;br/&gt;      tisch = _tisch; &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;void&lt;/span&gt; Würfel_übernehmen(&lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] wurf, &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] becher) { &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;var&lt;/span&gt; j = &lt;span class="hljs-number"&gt;0&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; x = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; x &amp;lt; &lt;span class="hljs-number"&gt;5&lt;/span&gt;; x++) { &lt;br/&gt;           &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (becher[x] != &lt;span class="hljs-number"&gt;0&lt;/span&gt;) { &lt;br/&gt;             becher[x] = wurf[j]; &lt;br/&gt;             j += &lt;span class="hljs-number"&gt;1&lt;/span&gt;; &lt;br/&gt;           } &lt;br/&gt;      } &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;void&lt;/span&gt; Würfel_umschalten(&lt;span class="hljs-keyword"&gt;int&lt;/span&gt; i, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] &lt;br/&gt;      becher, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] tisch) { &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_becher[i] == &lt;span class="hljs-number"&gt;0&lt;/span&gt;) { &lt;br/&gt;           _becher[i] = _tisch[i]; &lt;br/&gt;           _tisch[i] = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; &lt;br/&gt;      } &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;else&lt;/span&gt; { &lt;br/&gt;           _tisch[i] = _becher[i]; &lt;br/&gt;           _becher[i] = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; &lt;br/&gt;      } &lt;br/&gt;      becher = _becher; &lt;br/&gt;      tisch = _tisch; &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;void&lt;/span&gt; &lt;span class="hljs-title"&gt;Setzen&lt;/span&gt;(&lt;span class="hljs-params"&gt;Koordinate koordinate, &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;      &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; Tabelle tabelle, &lt;span class="hljs-keyword"&gt;out&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; spieler&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;var&lt;/span&gt; wert = Wert_ermitteln(_becher, _tisch); &lt;br/&gt;      _tabelle[_spieler].Zeilen[koordinate.Zeile]&lt;br/&gt;        [koordinate.Spalte] = wert; &lt;br/&gt;&lt;br/&gt;      Spieler_wechseln(); &lt;br/&gt;&lt;br/&gt;      tabelle = _tabelle[_spieler]; &lt;br/&gt;      spieler = _spieler + &lt;span class="hljs-number"&gt;1&lt;/span&gt;; &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;void&lt;/span&gt; &lt;span class="hljs-title"&gt;Spieler_wechseln&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      _spieler = _spieler == &lt;span class="hljs-number"&gt;0&lt;/span&gt; ? &lt;span class="hljs-number"&gt;1&lt;/span&gt; : &lt;span class="hljs-number"&gt;0&lt;/span&gt;; &lt;br/&gt;      _wurf = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; &lt;br/&gt;      _becher = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt;[] { &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt;, &lt;span class="hljs-number"&gt;7&lt;/span&gt; }; &lt;br/&gt;      _tisch = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt;[] { &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt; }; &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; &lt;span class="hljs-title"&gt;Wert_ermitteln&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] becher, &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;      &lt;span class="hljs-keyword"&gt;int&lt;/span&gt;[] tisch&lt;/span&gt;) &lt;/span&gt;{ &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;var&lt;/span&gt; würfel = becher.Where(x =&amp;gt; x != &lt;span class="hljs-number"&gt;0&lt;/span&gt;).Union(&lt;br/&gt;        tisch.Where(x =&amp;gt; x != &lt;span class="hljs-number"&gt;0&lt;/span&gt;)).ToArray(); &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; würfel.Sum(); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt; 
Es erfolgt keine inhaltliche Prüfung beim Setzen und die Summe wird noch nicht nach den Kniffelregeln berechnet. Als erstes Inkrement ist die Lösung dennoch brauchbar, weil der Product Owner bereits sein Feedback geben kann: Die Benutzerführung ist so weit implementiert, dass man das Spiel bedienen kann. Auch das Würfeln funktioniert bereits. Lediglich die Eingabe der Koordinaten beim Setzen des Wurfs wird bislang nicht überprüft. Gibt man hier fehlerhafte Werte ein, stürzt die Anwendung ab. Der Product Owner kann an dieser Stelle entscheiden, ob als Nächstes die Benutzerschnittstelle robust gemacht werden soll oder ob die Spielregeln weiter implementiert werden sollen.

Tests

Für die Klasse Kniffel habe ich Tests ergänzt. Allerdings zeigt sich hier eine weitere Schwachstelle meiner Bottom-up-Vorgehensweise: Dadurch, dass ich mich zunächst um das Würfeln gekümmert habe, hatte ich diese Funktionalität bereits, als ich begann, mir über den „Rest“ Gedanken zu machen. Das führte dazu, dass die Klasse Kniffel eine Abhängigkeit zur Klasse Würfelbecher hat. Das bedeutet allerdings, dass die Ergebnisse vom Zufallszahlengenerator abhängig sind, der in der Klasse ­Würfelbecher verwendet wird. Die Abhängigkeiten zeigt Bild 3.
Beim automatisierten Testen der Klasse Kniffel stellt sich das Problem, dass für einige Tests das Ergebnis des Würfelbechers stabil bleiben müsste. Die Würfel dürften im Test eben nicht zufällig fallen. Bei den Tests für den Würfelbecher habe ich dies berücksichtigt: Hier kann für die Tests eine Instanz von Random in den Konstruktor reingereicht werden. Listing 4 zeigt die Tests für den Würfelbecher.
Listing 4: Tests für den Würfelbecher
using System; &lt;br/&gt;using System.Collections.Generic; &lt;br/&gt;using FluentAssertions; &lt;br/&gt;using NUnit.Framework; &lt;br/&gt;&lt;br/&gt;namespace kniffel.game.tests &lt;br/&gt;{ &lt;br/&gt;  [TestFixture] &lt;br/&gt;  public class WürfelbecherTests &lt;br/&gt;  { &lt;br/&gt;    private Würfelbecher sut; &lt;br/&gt;&lt;br/&gt;    [SetUp] &lt;br/&gt;    public void Setup() { &lt;br/&gt;      sut = new Würfelbecher(new Random(&lt;span class="hljs-number"&gt;42&lt;/span&gt;)); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    [Test] &lt;br/&gt;    public void Random_42_liefert_immer_folgende_&lt;br/&gt;      Werte() { &lt;br/&gt;      var random = new Random(&lt;span class="hljs-number"&gt;42&lt;/span&gt;); &lt;br/&gt;      var result = new List&amp;lt;int&amp;gt;(); &lt;br/&gt;      for (int i = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class="hljs-number"&gt;10&lt;/span&gt;; i++) { &lt;br/&gt;         result.Add(random.Next(&lt;span class="hljs-number"&gt;1&lt;/span&gt;, &lt;span class="hljs-number"&gt;6&lt;/span&gt; + &lt;span class="hljs-number"&gt;1&lt;/span&gt;)); &lt;br/&gt;      } &lt;br/&gt;&lt;br/&gt;      result.Should().Equal(&lt;span class="hljs-number"&gt;5&lt;/span&gt;, &lt;span class="hljs-number"&gt;1&lt;/span&gt;, &lt;span class="hljs-number"&gt;1&lt;/span&gt;, &lt;span class="hljs-number"&gt;4&lt;/span&gt;, &lt;span class="hljs-number"&gt;2&lt;/span&gt;, &lt;span class="hljs-number"&gt;2&lt;/span&gt;, &lt;span class="hljs-number"&gt;5&lt;/span&gt;, &lt;br/&gt;        &lt;span class="hljs-number"&gt;4&lt;/span&gt;, &lt;span class="hljs-number"&gt;2&lt;/span&gt;, &lt;span class="hljs-number"&gt;5&lt;/span&gt;); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    [Test] &lt;br/&gt;    public void Wurf_besteht_aus_n_Würfeln() { &lt;br/&gt;      var result = sut.Würfeln(&lt;span class="hljs-number"&gt;5&lt;/span&gt;); &lt;br/&gt;      result.Should().HaveCount(&lt;span class="hljs-number"&gt;5&lt;/span&gt;); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    [Test] &lt;br/&gt;    public void Wurf_liefert_n_zufällig_Zahlen() { &lt;br/&gt;      var result = sut.Würfeln(&lt;span class="hljs-number"&gt;5&lt;/span&gt;); &lt;br/&gt;      result.Should().Equal(&lt;span class="hljs-number"&gt;5&lt;/span&gt;, &lt;span class="hljs-number"&gt;1&lt;/span&gt;, &lt;span class="hljs-number"&gt;1&lt;/span&gt;, &lt;span class="hljs-number"&gt;4&lt;/span&gt;, &lt;span class="hljs-number"&gt;2&lt;/span&gt;); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    [Test] &lt;br/&gt;    public void Es_kann_mehrfach_gewürfelt_werden() &lt;br/&gt;      { &lt;br/&gt;      var result = sut.Würfeln(&lt;span class="hljs-number"&gt;5&lt;/span&gt;); &lt;br/&gt;      result = sut.Würfeln(&lt;span class="hljs-number"&gt;1&lt;/span&gt;); &lt;br/&gt;      result.Should().Equal(&lt;span class="hljs-number"&gt;2&lt;/span&gt;); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt; 
Hier wird in der Setup-Methode eine Instanz der Klasse Random reingereicht, die immer die gleichen pseudozufälligen Werte liefert. Die zugehörige Implementation zeigt Listing 5.Nun könnte ich das gleiche Verfahren auf die Klasse Kniffel anwenden und für Tests eine Instanz von Random oder Würfelbecher reinreichen. Das beruht dann allerdings auf der überkommenen Vorgehensweise, die solche Abhängigkeiten akzeptiert und dann nach Lösungen sucht, die Abhängigkeitsstruktur testbar zu machen. Viel besser ist es dagegen, die Abhängigkeit zu eliminieren, dann stellt sich die Herausforderung beim Testen erst gar nicht.
Listing 5: Pseudozufallswerte über die Klasse Random liefern
&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.Collections.Generic; &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; kniffel.game &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; Würfelbecher &lt;br/&gt;  { &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; readonly IEnumerator&amp;lt;&lt;span class="hljs-keyword"&gt;int&lt;/span&gt;&amp;gt; enumerator; &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-keyword"&gt;public&lt;/span&gt; Würfelbecher() &lt;br/&gt;      : &lt;span class="hljs-keyword"&gt;this&lt;/span&gt;(&lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Random(DateTime.Now.Millisecond)) { &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    internal Würfelbecher(Random &lt;span class="hljs-built_in"&gt;random&lt;/span&gt;) { &lt;br/&gt;      enumerator = NächsterWurfEnumerable(&lt;span class="hljs-built_in"&gt;random&lt;/span&gt;).&lt;br/&gt;      GetEnumerator(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; IEnumerable&amp;lt;&lt;span class="hljs-keyword"&gt;int&lt;/span&gt;&amp;gt; NächsterWurfEnumerable(&lt;br/&gt;      Random &lt;span class="hljs-built_in"&gt;random&lt;/span&gt;) { &lt;br/&gt;&lt;br/&gt;      &lt;span class="hljs-built_in"&gt;while&lt;/span&gt; (true) { &lt;br/&gt;        &lt;span class="hljs-built_in"&gt;yield&lt;/span&gt; &lt;span class="hljs-built_in"&gt;return&lt;/span&gt; &lt;span class="hljs-built_in"&gt;random&lt;/span&gt;.Next(&lt;span class="hljs-number"&gt;1&lt;/span&gt;, &lt;span class="hljs-number"&gt;6&lt;/span&gt; + &lt;span class="hljs-number"&gt;1&lt;/span&gt;); &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;int&lt;/span&gt; NächsterWurf() { &lt;br/&gt;      enumerator.MoveNext(); &lt;br/&gt;      &lt;span class="hljs-built_in"&gt;return&lt;/span&gt; enumerator.Current; &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;int&lt;/span&gt;[] Würfeln(&lt;span class="hljs-keyword"&gt;int&lt;/span&gt; AnzahlWürfel) { &lt;br/&gt;      var result = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; List&amp;lt;&lt;span class="hljs-keyword"&gt;int&lt;/span&gt;&amp;gt;(); &lt;br/&gt;&lt;br/&gt;      &lt;span class="hljs-built_in"&gt;for&lt;/span&gt; (var i = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; i &amp;lt; AnzahlWürfel; i++) { &lt;br/&gt;        result.Add(NächsterWurf()); &lt;br/&gt;      } &lt;br/&gt;      &lt;span class="hljs-built_in"&gt;return&lt;/span&gt; result.ToArray(); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt; 
Für die Aufgabenverteilung der Klassen Kniffel und Würfelbecher kann die Abhängigkeit auch gelöst werden, wie in Bild 4 zu sehen ist.In dieser Abhängigkeitsstruktur sind das Integration Operation Segregation Principle (IOSP) sowie das Principle of Mutual Oblivion (PoMO) eingehalten. Nun ist es Aufgabe der Klasse Spiel, die beiden Aspekte Kniffel und Würfelbecher zu integrieren.
Die Klasse Spiel enthält nur Code zur Integra­tion der beiden anderen Klassen. Kniffel und Würfelbecher dagegen enthalten nur Domänenlogik und keinen Code zur Integration. Wenn wir nun noch berücksichtigen, dass die Klasse Interactors bislang lediglich die Klasse Kniffel integriert, wird klar, dass wir den Codeanteil der Klasse Kniffel, der für die Integration des Würfelbechers zuständig ist, in den Interactor auslagern können.

Zwischenfazit

An dieser Stelle wurde die Übungsaufgabe für mich wirklich ungemütlich. Ich musste bemerken, dass durch den anfäng­lichen Bottom-up-Ansatz einiges schiefgegangen war. Insbesondere hatte ich es versäumt, die Entwürfe der obersten Ebene zu verfeinern, bevor ich mit der Implementation begann.

Entwurf verfeinert

In Bild 5 ist zu sehen, wie ich den Entwurf der Interaktion Würfeln verfeinert habe. Nun sind die Zuständigkeiten klar getrennt. Vor allem kommt es in der Klasse Kniffel nicht mehr zur Vermischung von Aspekten. Sie ist nur noch für das Kniffelspiel zuständig.
Das Würfeln ist nach wie vor in der Klasse Würfelbecher realisiert. Allerdings ist die Integration dieser beiden Aspekte nun in den Interactor gewandert. Folgendes Listing zeigt die relevante Änderung im Interactor:
public void Würfeln(out int[] becher, out int[] tisch) { 
  int[] be = null; 
  int[] ti = null; 
 
  kniffel.Wurf_Voraussetzungen_prüfen( 
    fertig: (b, t) =&gt; { be = b; ti = t; }, 
    weiter: (anzahl) =&gt; { 
      var wurf = würfelbecher.Würfeln(anzahl); 
      kniffel.Würfel_übernehmen(wurf, out be, out ti); 
    } 
  ); 
  becher = be; 
  tisch = ti; 
} 
Etwas unschön ist die Tatsache, dass die out-Parameter nicht direkt in den Lambda Expressions zugewiesen werden können. Dies lässt mich noch mal überdenken, ob out-Parameter eine gute Lösung sind, um mehrere Werte zurückzuliefern. Die Alternative wäre die Verwendung von Tuple<> oder einer eigenen Datenstruktur. Vielleicht hilft auch die bevor­stehende C#-7.0-Syntax, die es gestattet, Tuple mittels return (x, y) zurückzugeben.

Fazit

Es ist immer wieder lehrreich, eine Übungsaufgabe bewusst „falsch“ anzugehen. Das setzt natürlich voraus, dass man zuvor den richtigen Weg einige Male geübt hat. Andernfalls würde man den Unterschied nicht bemerken. Durch meine bewusste Bottom-up-Vorgehensweise und den anfänglichen Verzicht auf die Verfeinerung des Entwurfs haben sich an einigen Stellen Probleme in der Implementation ergeben. Wieder einmal wies mich die mangelnde Testbarkeit darauf hin, dass die Umsetzung nicht optimal war. Diesen Effekt beobachte ich in Workshops immer wieder: Lässt sich der Code nicht leicht automatisiert testen, ist dies fast immer ein Hinweis darauf, dass vor der Implementation nicht sauber entworfen wurde. Und ich muss sagen: Es war diesmal mühsam, die Lösung dann im Nachhinein einigermaßen „geradezubiegen“. Nächstes Mal also wieder top-down und mit Entwurf.Noch eine kurze Bemerkung zur Lösung auf der Heft-CD: Meine Lösung ist mit .NET Core realisiert. Sie müssen daher Ihr Visual Studio mit entsprechenden Updates versorgen, bevor Sie die Lösung öffnen können.
Projektdateien

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
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige