Eine Aufgabe in meiner Software-Engineering-Klasse ist es, eine Anwendung zu entwerfen, die verschiedene Formen eines bestimmten Spiels spielen kann. Das fragliche Spiel ist Mancala, einige dieser Spiele heißen Wari oder Kalah. Diese Spiele unterscheiden sich in einigen Aspekten, aber für meine Frage ist es nur wichtig zu wissen, dass sich die Spiele in folgenden Punkten unterscheiden können:
- Die Art und Weise, wie das Ergebnis eines Umzugs behandelt wird
- Die Art und Weise, in der das Spielende bestimmt wird
- Die Art und Weise, in der der Gewinner ermittelt wird
Das erste, woran ich dachte, war, das Strategiemuster zu verwenden. Ich habe eine Variation der Algorithmen (die eigentlichen Spielregeln). Das Design könnte so aussehen:
Ich dachte mir dann, dass im Spiel von Mancala und Wari die Art und Weise, wie der Gewinner ermittelt wird, genau dieselbe ist und der Code dupliziert würde. Ich denke nicht, dass dies per definitionem eine Verletzung des Grundsatzes "eine Regel, ein Ort" oder "trocken" ist, da eine Änderung der Regeln für Mancala nicht automatisch bedeutet, dass die Regel auch in Wari geändert werden sollte. Trotzdem hatte ich aufgrund der Rückmeldungen meines Professors den Eindruck, ein anderes Design zu finden.
Ich habe mir dann folgendes ausgedacht:
Jedes Spiel (Mancala, Wari, Kalah, ...) hätte nur ein Attribut des Typs der Schnittstelle jeder Regel, dh WinnerDeterminer
und wenn es eine Mancala 2.0-Version gibt, die mit Mancala 1.0 identisch ist, außer wie der Gewinner bestimmt wird, kann dies nur sein Verwenden Sie die Mancala-Versionen.
Ich denke, die Implementierung dieser Regeln als Strategiemuster ist zweifellos gültig. Das eigentliche Problem ist jedoch, dass ich es weiterentwickeln möchte.
Beim Lesen des Schablonenmethodenmusters dachte ich sofort, dass es auf dieses Problem angewendet werden könnte. Die Aktionen, die ausgeführt werden, wenn ein Benutzer einen Zug ausführt, sind immer gleich und in derselben Reihenfolge:
- Steine in Löcher ablegen (dies ist für alle Spiele gleich, würde also in der Template-Methode selbst implementiert werden)
- Ermitteln Sie das Ergebnis des Umzugs
- Ermitteln Sie, ob das Spiel aufgrund des vorherigen Zugs beendet wurde
- Wenn das Spiel beendet ist, bestimmen Sie, wer gewonnen hat
Diese drei letzten Schritte sind alle in meinem oben beschriebenen Strategiemuster enthalten. Ich habe große Probleme, diese beiden zu kombinieren. Eine mögliche Lösung, die ich gefunden habe, wäre, das Strategiemuster aufzugeben und Folgendes zu tun:
Ich sehe nicht wirklich den Designunterschied zwischen dem Strategiemuster und diesem? Ich bin mir aber sicher, dass ich eine Template-Methode verwenden muss (obwohl ich mir genauso sicher war, ein Strategiemuster verwenden zu müssen).
Ich kann auch nicht bestimmen, wer für die Erstellung des TurnTemplate
Objekts verantwortlich ist, während ich mit dem Strategiemuster das Gefühl habe, dass ich Objektfamilien (die drei Regeln) habe, die ich leicht mit einem abstrakten Factory-Muster erstellen könnte. Ich hätte dann ein MancalaRuleFactory
, WariRuleFactory
usw. und sie würden die richtigen Instanzen der Regeln erstellen und mir ein RuleSet
Objekt zurückgeben.
Angenommen, ich verwende die Strategie + abstraktes Factory-Muster und habe ein RuleSet
Objekt, das Algorithmen für die drei Regeln enthält. Ich habe nur das Gefühl, dass ich das Muster der Template-Methode weiterhin verwenden kann, indem ich dieses RuleSet
Objekt an meine übergebe TurnTemplate
. Das "Problem", das dann auftaucht, ist, dass ich meine konkreten Implementierungen von den TurnTemplate
, diesen Klassen niemals brauchen würde, obsolet werden würde. Bei meinen geschützten Methoden TurnTemplate
konnte ich das einfach aufrufen ruleSet.determineWinner()
. In der Folge TurnTemplate
wäre die Klasse nicht mehr abstrakt, sondern müsste konkretisiert werden, ist sie dann noch ein Template-Methodenmuster?
Fassen wir zusammen: Denke ich richtig oder fehle ich etwas Leichtes? Wie kann ich ein Strategie- und ein Vorlagenmethodenmuster kombinieren, wenn ich auf dem richtigen Weg bin?
quelle
Antworten:
Nachdem Sie sich Ihre Entwürfe angesehen haben, erscheinen sowohl die erste als auch die dritte Iteration als elegantere Entwürfe. Sie erwähnen jedoch, dass Sie ein Student sind und Ihr Professor Ihnen Feedback gegeben hat. Ohne genau zu wissen, was Ihre Aufgabe oder der Zweck der Klasse ist, oder mehr darüber, was Ihr Professor vorgeschlagen hat, würde ich alles, was ich unten sage, mit einem Körnchen Salz nehmen.
In Ihrem ersten Entwurf erklären Sie
RuleInterface
, dass Sie eine Benutzeroberfläche sind, die definiert, wie der Zug jedes Spielers behandelt wird, wie festgestellt wird, ob das Spiel beendet ist und wie ein Gewinner nach dem Ende des Spiels ermittelt wird. Es scheint, dass dies eine gültige Schnittstelle zu einer Familie von Spielen ist, die Variationen erfahren. Abhängig von den Spielen haben Sie möglicherweise Code dupliziert. Ich würde zustimmen, dass die Flexibilität, die Regeln eines Spiels zu ändern, eine gute Sache ist, aber ich würde auch argumentieren, dass die Vervielfältigung von Code fürchterlich für Mängel ist. Wenn Sie fehlerhaften Code zwischen Implementierungen kopieren / einfügen und einer einen Fehler enthält, müssen Sie jetzt mehrere Fehler an verschiedenen Stellen beheben. Wenn Sie die Implementierungen zu unterschiedlichen Zeiten umschreiben, können an verschiedenen Stellen Fehler auftreten. Beides ist nicht wünschenswert.Ihr zweites Design wirkt recht komplex mit einem tiefen Vererbungsbaum. Zumindest ist es tiefer, als ich erwarten würde, um diese Art von Problem zu lösen. Sie beginnen auch, Implementierungsdetails in andere Klassen aufzuteilen. Letztendlich modellieren und implementieren Sie ein Spiel. Dies könnte ein interessanter Ansatz sein, wenn Sie aufgefordert werden, Ihre Regeln zu kombinieren, um die Ergebnisse eines Zuges, das Spielende und einen Sieger zu ermitteln, der nicht den von Ihnen genannten Anforderungen entspricht . Ihre Spiele sind klar definierte Regelwerke, und ich würde versuchen, die Spiele so weit wie möglich in separate Einheiten zu unterteilen.
Ihr dritter Entwurf gefällt mir am besten. Meine einzige Sorge ist, dass es nicht auf der richtigen Abstraktionsebene ist. Im Moment scheinen Sie eine Wende zu modellieren. Ich würde empfehlen, über das Design des Spiels nachzudenken. Denken Sie daran, dass Sie Spieler haben, die mit Steinen Züge auf einem Brett machen. Für Ihr Spiel müssen diese Schauspieler anwesend sein. Von dort ist der Algorithmus nicht
doTurn()
aberplayGame()
, die von dem anfänglichen Schritt zum endgültigen Umzug geht, nach dem es endet. Nach jedem Zug des Spielers wird der Status des Spiels angepasst, es wird festgestellt, ob sich das Spiel in einem endgültigen Status befindet, und wenn dies der Fall ist, wird der Gewinner ermittelt.Ich würde empfehlen, Ihre ersten und dritten Entwürfe näher zu betrachten und mit ihnen zu arbeiten. Es könnte auch hilfreich sein, in Prototypen zu denken. Wie würden die Clients aussehen, die diese Schnittstellen verwenden? Ist ein Entwurfsansatz sinnvoller für die Implementierung eines Clients, der tatsächlich ein Spiel instanziiert und das Spiel spielt? Sie müssen erkennen, womit es interagiert. In Ihrem speziellen Fall handelt es sich um die
Game
Klasse und alle anderen zugehörigen Elemente. Sie können nicht isoliert entwerfen.Da Sie erwähnen, dass Sie ein Student sind, möchte ich einige Dinge aus einer Zeit mitteilen, als ich TA für einen Software-Design-Kurs war:
quelle
GameTemplate
das sich viel besser anfühlt. Außerdem kann ich eine Factory-Methode kombinieren, um Spieler, das Board usw. zu initialisieren.Ihre Verwirrung ist berechtigt. Die Sache ist, dass sich Muster nicht gegenseitig ausschließen.
Die Template-Methode ist die Basis für eine Reihe anderer Muster, wie z. B. Strategie und Status. Grundsätzlich enthält die Strategy-Schnittstelle eine oder mehrere Template-Methoden, für die alle Objekte, die eine Strategie implementieren, (mindestens) so etwas wie eine doAction () -Methode haben müssen. Dadurch können die Strategien gegeneinander ausgetauscht werden.
In Java ist eine Schnittstelle nichts anderes als eine Reihe von Vorlagenmethoden. Ebenso ist jede abstrakte Methode im Wesentlichen eine Template-Methode. Dieses Muster war (unter anderem) den Designern der Sprache gut bekannt, also bauten sie es ein.
@ThomasOwens bietet exzellente Ratschläge zur Lösung Ihres speziellen Problems.
quelle
Wenn Sie von Designmustern abgelenkt werden, würde ich Ihnen raten, zuerst den Prototyp des Spiels zu erstellen, und dann sollten Muster einfach auf Sie losgehen. Ich denke nicht, dass es wirklich möglich oder ratsam ist, zuerst zu versuchen, ein System perfekt zu entwerfen und es dann zu implementieren (auf ähnliche Weise finde ich es verwirrend, wenn Leute versuchen, zuerst ganze Programme zu schreiben und dann zu kompilieren, anstatt es Stück für Stück zu tun Das Problem ist, dass Sie wahrscheinlich nicht an jedes Szenario denken, mit dem sich Ihre Logik befassen muss, und dass Sie während der Implementierungsphase entweder die Hoffnung verlieren oder versuchen, an Ihrem ursprünglichen fehlerhaften Design festzuhalten und Hacks einzuführen oder sogar Schlimmer noch, gib gar nichts ab.
quelle
Kommen wir zu den Messingnägeln. Es ist absolut keine Spieloberfläche, keine Entwurfsmuster, keine abstrakten Klassen und keine UML erforderlich.
Wenn Sie eine angemessene Anzahl von Support-Klassen haben, wie z. B. Benutzeroberfläche, Simulation und was auch immer, wird der gesamte Code, der nicht für die Spielelogik spezifisch ist, sowieso wiederverwendet. Außerdem ändert Ihr Benutzer sein Spiel nicht dynamisch. Zwischen den Spielen wird nicht mit 30 Hz gewechselt. Sie spielen eine halbe Stunde lang ein Spiel. Ihr "dynamischer" Polymorphismus ist also überhaupt nicht dynamisch. Es ist eher statisch.
Der vernünftige Weg hierher ist es, eine generische funktionale Abstraktion wie C #
Action
oder C ++ zu verwendenstd::function
, eine Mancala-, eine Wari- und eine Kalah-Klasse zu erstellen und von dort fortzufahren.Erledigt.
Sie nennen Spiele nicht. Spiele rufen dich an.
quelle