Wie lässt sich diese reale Aktivität modellieren, die in OOP Zirkelverweise zu benötigen scheint?

24

Ich habe mit einem Problem in einem Java-Projekt über Zirkelverweise gerungen. Ich versuche, eine reale Situation zu modellieren, in der die fraglichen Objekte voneinander abhängig zu sein scheinen und voneinander wissen müssen.

Das Projekt ist ein allgemeines Modell für das Spielen eines Brettspiels. Die Grundklassen sind nicht spezifisch, werden jedoch erweitert, um Besonderheiten von Schach, Backgammon und anderen Spielen zu behandeln. Ich habe dies vor 11 Jahren als Applet mit einem halben Dutzend verschiedener Spiele programmiert, aber das Problem ist, dass es voller Zirkelverweise ist. Ich habe es damals implementiert, indem ich alle miteinander verflochtenen Klassen in eine einzige Quelldatei gestopft habe, aber ich habe die Idee, dass das in Java eine schlechte Form ist. Jetzt möchte ich etwas Ähnliches wie eine Android-App implementieren und die Dinge richtig machen.

Die Klassen sind:

  • Regelbuch: Ein Objekt, das nach bestimmten Kriterien abgefragt werden kann, z. B. nach dem anfänglichen Layout des Bretts, nach anderen Informationen zum anfänglichen Spielstatus, z eine aktuelle oder vorgeschlagene Vorstandsposition.

  • Brett: Eine einfache Darstellung eines Spielbretts, das angewiesen werden kann, einen Zug zu reflektieren.

  • MoveList: Eine Liste von Moves. Dies hat zwei Ziele: eine Auswahl von Zügen, die zu einem bestimmten Zeitpunkt verfügbar sind, oder eine Liste von Zügen, die im Spiel ausgeführt wurden. Es könnte in zwei nahezu identische Klassen unterteilt werden, aber das ist für die von mir gestellte Frage nicht relevant und kann die Sache noch komplizierter machen.

  • Move: ein einziger Zug. Es enthält alles über den Zug als eine Liste von Atomen: nimm ein Stück von hier, lege es dort ab, entferne ein gefangenes Stück von dort.

  • Status: Die vollständigen Statusinformationen eines laufenden Spiels. Nicht nur die Board-Position, sondern auch eine MoveList und andere Statusinformationen, z. B. wer jetzt umziehen soll. Im Schach würde man notieren, ob der König und die Türme jedes Spielers bewegt worden sind.

Zirkuläre Verweise gibt es zuhauf, zum Beispiel: Das Regelbuch muss den Spielstatus kennen, um zu bestimmen, welche Züge zu einem bestimmten Zeitpunkt verfügbar sind, aber der Spielstatus muss das Regelbuch nach dem anfänglichen Startlayout und den mit einem Zug einhergehenden Nebenwirkungen abfragen es wird gemacht (zB wer zieht als nächstes).

Ich habe versucht, die neuen Klassen hierarchisch zu organisieren, wobei RuleBook an der Spitze stand, da es über alles Bescheid wissen muss. Dies hat jedoch zur Folge, dass viele Methoden in die RuleBook-Klasse verschoben werden müssen (z. B. Verschieben), wodurch sie monolithisch und nicht besonders repräsentativ für das ist, was ein RuleBook sein sollte.

Was ist also der richtige Weg, um dies zu organisieren? Sollte ich RuleBook in BigClassThatDoesAlmostEverythingInTheGame umwandeln, um Zirkelverweise zu vermeiden, und den Versuch aufgeben, das reale Spiel genau zu modellieren? Oder sollte ich mich an die voneinander abhängigen Klassen halten und den Compiler dazu überreden, sie irgendwie zu kompilieren und dabei mein reales Modell beizubehalten? Oder gibt es eine offensichtlich gültige Struktur, die mir fehlt?

Vielen Dank für jede Hilfe, die Sie geben können!

Damian Walker
quelle
7
Was wäre, wenn die RuleBookz. B. das Stateals Argument nehmen und das gültige zurückgeben würden MoveList, dh "hier sind wir jetzt, was kann als nächstes getan werden?"
Jonrsharpe
Was @jonrsharpe gesagt hat. Wenn Sie ein echtes Brettspiel spielen, sind dem Regelwerk auch keine tatsächlichen Spiele bekannt. Ich würde wahrscheinlich sogar eine andere Klasse einführen, um Bewegungen tatsächlich zu berechnen, aber das könnte davon abhängen, wie groß diese RuleBook-Klasse bereits ist.
Sebastiaan van den Broek
4
Das Vermeiden des God-Objekts (BigClassThatDoesAlmostEverythingInTheGame) ist viel wichtiger als das Vermeiden von Zirkelverweisen.
user281377
2
@ user281377 sie schließen sich jedoch nicht unbedingt gegenseitig aus!
jonrsharpe
1
Können Sie die Modellierungsversuche zeigen? Ein Diagramm zum Beispiel?
Benutzer

Antworten:

47

Ich habe mit einem Problem in einem Java-Projekt über Zirkelverweise gerungen.

Javas Garbage Collector ist nicht auf Referenzzähltechniken angewiesen. Zirkelverweise verursachen in Java keinerlei Probleme. Die Zeit, die aufgewendet wird, um vollkommen natürliche Zirkelverweise in Java zu entfernen, ist Zeitverschwendung.

Ich habe dies [...] verschlüsselt, aber das Problem ist, dass es voller Zirkelverweise ist. Ich habe es damals implementiert, indem ich alle ineinander verschlungenen Klassen in einer einzigen Quelldatei [...]

Nicht nötig. Wenn Sie nur alle Quelldateien auf einmal kompilieren (z. B. javac *.java), löst der Compiler alle Vorwärtsreferenzen problemlos auf.

Oder sollte ich mich an die voneinander abhängigen Klassen halten und den Compiler dazu bringen, sie irgendwie zu kompilieren, [...]

Ja. Es wird erwartet, dass die Anwendungsklassen voneinander abhängig sind. Kompilieren aller Dateien Java - Quellcode , die auf zu demselben Paket gehören einmal ist nicht ein kluger Hack, es ist genau so , wie Java soll an die Arbeit.

Atsby
quelle
24
"Zirkelverweise verursachen in Java keinerlei Probleme." In Bezug auf die Zusammenstellung ist dies wahr. Zirkelverweise gelten jedoch als schlechtes Design .
Chop
22
Zirkuläre Verweise sind in vielen Situationen völlig selbstverständlich. Deshalb verwenden Java und andere moderne Sprachen einen ausgeklügelten Garbage Collector anstelle eines einfachen Referenzzählers.
user281377
3
Java ist in der Lage, Zirkelverweise aufzulösen, und es ist definitiv richtig, dass sie in vielen Situationen natürlich sind. Aber OP präsentierte eine spezifische Situation, und das sollte berücksichtigt werden. Verwirrter Spaghetti-Code ist wahrscheinlich nicht der beste Weg, um dieses Problem zu lösen.
Matthew Read
3
Bitte verbreiten Sie keine unbegründeten FUD über nicht verwandte Programmiersprachen. Python unterstützt seit jeher die GC von Referenzzyklen ( docs , auch auf SO: hier und hier ).
Christian Aichinger
2
Diese Antwort ist meiner Meinung nach nur mittelmäßig, da es kein einziges Wort darüber gibt, dass Zirkelverweise zum Beispiel für das OP nützlich sind.
Doc Brown
22

Zugegeben, kreisförmige Abhängigkeiten sind aus gestalterischer Sicht eine fragwürdige Praxis, aber sie sind nicht verboten, und aus rein technischer Sicht sind sie nicht unbedingt problematisch , wie Sie meinen: Sie sind vollkommen legal in In den meisten Szenarien sind sie in bestimmten Situationen unvermeidlich, und in einigen seltenen Fällen können sie sogar als nützlich angesehen werden.

Tatsächlich gibt es sehr wenige Szenarien, in denen der Java-Compiler eine zirkuläre Abhängigkeit verweigert. (Hinweis: Möglicherweise gibt es noch weitere, mir fällt momentan nur Folgendes ein.)

  1. In der Vererbung: Sie können Klasse A nicht erweitern Klasse B, die wiederum Klasse A erweitert, und es ist durchaus vernünftig, dass Sie dies nicht haben können, da die Alternative aus logischer Sicht absolut keinen Sinn ergibt.

  2. Unter methodenlokalen Klassen: Innerhalb einer Methode deklarierte Klassen verweisen möglicherweise nicht zirkulär aufeinander. Dies ist wahrscheinlich nichts anderes als eine Einschränkung des Java-Compilers, möglicherweise, weil die Fähigkeit, so etwas zu tun, nicht nützlich genug ist, um die zusätzliche Komplexität zu rechtfertigen, die in den Compiler gehen müsste, um ihn zu unterstützen. (Die meisten Java-Programmierer sind sich nicht einmal der Tatsache bewusst, dass Sie eine Klasse innerhalb einer Methode deklarieren können, geschweige denn mehrere Klassen deklarieren und diese Klassen dann zirkulär aufeinander verweisen lassen.)

Es ist daher wichtig zu erkennen, dass das Streben nach Minimierung von zirkulären Abhängigkeiten ein Streben nach Designreinheit und nicht nach technischer Korrektheit ist.

Soweit ich weiß, gibt es keinen reduktionistischen Ansatz zur Beseitigung zirkulärer Abhängigkeiten, was bedeutet, dass es kein Rezept gibt, das aus nichts als einfachen, vorher festgelegten, einfachen Schritten besteht, um ein System mit zirkulären Referenzen zu erstellen, diese nacheinander anzuwenden und zu beenden mit einem System ohne Zirkelverweise. Sie müssen sich an die Arbeit machen und Refactoring-Schritte durchführen, die von der Art Ihres Designs abhängen.

In der besonderen Situation, die Sie zur Hand haben, scheint es mir, dass Sie eine neue Entität benötigen, die vielleicht "Spiel" oder "GameLogic" genannt wird und die alle anderen Entitäten kennt (ohne dass es eine der anderen Entitäten weiß, ), damit sich die anderen Entitäten nicht kennen müssen.

Ich halte es zum Beispiel für unvernünftig, dass Ihre RuleBook-Entität irgendetwas über die GameState-Entität wissen muss, da ein Regelbuch etwas ist, das wir zum Spielen heranziehen, es ist nicht etwas, das aktiv am Spielen teilnimmt. Es ist also diese neue "Spiel" -Entität, die sowohl das Regelbuch als auch den Spielstatus konsultieren muss, um zu bestimmen, welche Züge verfügbar sind, und dies beseitigt die zirkulären Abhängigkeiten.

Nun, ich denke, ich kann mir vorstellen, was Ihr Problem mit diesem Ansatz sein wird: Das Codieren der "Spiel" -Entität in einer spielunabhängigen Weise wird sehr schwierig sein, so dass Sie höchstwahrscheinlich nicht nur mit einem, sondern mit zwei enden werden Entitäten, die maßgeschneiderte Implementierungen für jeden Spieltyp benötigen: das "RuleBook" und die "Game" -Entität. Was wiederum den Zweck zunichte macht, eine "RuleBook" -Entität zu haben. Nun, alles, was ich dazu sagen kann, ist, dass Ihr anfängliches Bestreben, ein System zu schreiben, das viele verschiedene Arten von Spielen spielen kann, vielleicht nobel, aber vielleicht schlecht konzipiert war. Wenn ich in Ihren Füßen stünde, hätte ich mich darauf konzentriert, einen gemeinsamen Mechanismus zum Anzeigen des Status aller verschiedenen Spiele und einen gemeinsamen Mechanismus zum Empfangen von Benutzereingaben für alle diese Spiele zu verwenden.

Mike Nakis
quelle
1
Danke Mike. Sie haben Recht mit den Nachteilen der Spieleinheit. Mit dem alten Applet-Code konnte ich mit etwas mehr als einer neuen RuleBook-Unterklasse und dem entsprechenden Grafikdesign neue Spiele erstellen.
Damian Walker
10

In der Spieltheorie werden Spiele als Liste vorheriger Züge (Werttypen einschließlich der Spieler) und als Funktion ValidMoves (previousMoves) behandelt.

Ich würde versuchen, diesem Muster für den nicht-UI-Teil des Spiels zu folgen und Dinge wie das Einrichten des Bretts als Moves zu behandeln.

Die Benutzeroberfläche kann dann ein Standard-OO-Zeugs sein, mit einer Möglichkeit, sich auf die Logik zu beziehen


Aktualisieren, um Kommentare zu komprimieren

Betrachten Sie Schach. Schachpartien werden üblicherweise als Zuglisten aufgezeichnet. http://en.wikipedia.org/wiki/Portable_Game_Notation

Die Liste der Züge definiert den vollständigen Zustand des Spiels viel besser als ein Bild des Spielbretts.

Nehmen wir zum Beispiel an, wir fangen an, Objekte für Board, Piece, Move usw. und Methoden wie Piece.GetValidMoves () zu erstellen.

Zuerst sehen wir, dass wir ein Stück Referenz auf die Tafel haben müssen, aber dann überlegen wir, ob wir Rochade spielen sollen. Was Sie nur tun können, wenn Sie Ihren König oder Turm noch nicht bewegt haben. Wir brauchen also eine MovedAlready-Flagge auf dem König und den Türmen. Ebenso können Bauern im ersten Zug 2 Felder ziehen.

Dann sehen wir, dass beim Rochieren der gültige Zug des Königs von der Existenz und dem Zustand des Turms abhängt. Das Board muss also mit Stücken versehen sein und sich auf diese beziehen. Wir befassen uns mit Ihrem Problem mit Rundschreiben.

Wenn wir jedoch Move als unveränderliche Struktur und unveränderlichen Spielstatus definieren, wie in der Liste der vorherigen Züge angegeben, verschwinden diese Probleme. Um zu sehen, ob die Rochade gültig ist, können wir die Zugliste der vorhandenen Burg- und Königszüge überprüfen. Um zu sehen, ob der Bauer en passent sein kann, können wir überprüfen, ob der andere Bauer zuvor einen doppelten Zug ausgeführt hat. Es werden keine Referenzen benötigt, außer Regeln -> Verschieben

Jetzt hat Schach ein statisches Brett, und die Peices werden immer auf die gleiche Weise eingerichtet. Nehmen wir jedoch an, wir haben eine Variante, in der wir eine alternative Einrichtung zulassen. vielleicht einige Stücke als Handicap weglassen.

Wenn wir die Setup-Züge als Züge hinzufügen, 'vom Kästchen zum Quadrat X' und das Rules-Objekt anpassen, um diesen Zug zu verstehen, können wir das Spiel dennoch als eine Folge von Zügen darstellen.

Wenn in Ihrem Spiel das Spielfeld selbst nicht statisch ist, können Sie beispielsweise Schachfelder hinzufügen oder Felder vom Spielfeld entfernen, damit sie nicht verschoben werden können. Diese Änderungen können auch als Verschiebungen dargestellt werden, ohne dass die Gesamtstruktur der Regelengine geändert werden muss oder auf ein BoardSetup- Objekt ähnlicher Art verwiesen werden muss

Ewan
quelle
Dies erschwert in der Regel die Implementierung von ValidMoves, wodurch Ihre Logik verlangsamt wird.
Taemyr
nicht wirklich, ich gehe davon aus, dass das Board-Setup variabel ist, also musst du es irgendwie definieren. Wenn Sie das Setup in eine andere Struktur oder ein anderes Objekt konvertieren, um die Berechnung zu erleichtern, können Sie das Ergebnis bei Bedarf zwischenspeichern. Einige Spiele haben Spielbretter, die sich mit dem Spiel ändern, und einige gültige Züge können eher von vorherigen Zügen als von der aktuellen Position abhängen (z. B. Rochade im Schach)
Ewan
1
Das Hinzufügen von Flags und anderen Elementen ist die Komplexität, die Sie vermeiden, wenn Sie nur den Bewegungsverlauf haben. Es ist nicht teuer, eine Runde über 100 Schachzüge zu
Ewan,
1
Sie vermeiden außerdem, Ihr Objektmodell so zu ändern, dass es Regeln widerspiegelt. Wenn Sie also für Schach validMoves -> Piece + Board ausführen, scheitern Sie am Casting, am Passieren, am ersten Zug für Bauern und an der Stückwerbung und müssen den Objekten zusätzliche Informationen hinzufügen oder ein drittes Objekt referenzieren. Sie verlieren auch die Idee, wer es ist und Konzepte wie entdeckte Scheck
Ewan
1
@Gabe The boardLayoutist eine Funktion von allen priorMoves(dh wenn wir es als state beibehalten würden, würde nichts anderes als jedes beigetragen thisMove). Daher ist Ewans Vorschlag im Wesentlichen "cut the middle man" - gültige Bewegungen eine direkte Funktion aller vorherigen, anstatt validMoves( boardLayout( priorMoves ) ).
ABl. Vom
8

Die Standardmethode zum Entfernen eines Zirkelverweises zwischen zwei Klassen in der objektorientierten Programmierung besteht darin, eine Schnittstelle einzuführen, die dann von einer von ihnen implementiert werden kann. In Ihrem Fall könnten Sie also einen RuleBookVerweis haben, Stateder sich dann auf eine InitialPositionProvider(von implementierte RuleBook) Schnittstelle bezieht . Dies erleichtert auch das Testen, da Sie dann eine erstellen können, die zu Testzwecken Stateeine andere (vermutlich einfachere) Ausgangsposition verwendet.

Jules
quelle
6

Ich glaube, die Zirkelverweise und das Gott-Objekt in Ihrem Fall könnten leicht entfernt werden, indem die Steuerung des Spielflusses von den Staats- und Regelmodellen des Spiels getrennt wird. Auf diese Weise würden Sie wahrscheinlich viel Flexibilität gewinnen und unnötige Komplexität beseitigen.

Ich denke, Sie sollten einen Controller haben ("einen Spielleiter", wenn Sie möchten), der den Spielfluss steuert und aktuelle Statusänderungen handhabt, anstatt dem Regelwerk oder dem Spielstatus diese Verantwortung zu übertragen.

Ein Spielstatusobjekt muss sich nicht selbst ändern oder die Regeln kennen. Die Klasse muss lediglich ein Modell für leicht zu handhabende (erstellte, überprüfte, geänderte, dauerhafte, protokollierte, kopierte, zwischengespeicherte usw.) und effiziente Spielstatusobjekte für den Rest der Anwendung bereitstellen.

Das Regelwerk sollte kein laufendes Spiel kennen oder damit experimentieren müssen. Es sollte nur eine Sicht auf einen Spielstatus erforderlich sein, um feststellen zu können, welche Züge zulässig sind, und es muss nur mit einem resultierenden Spielstatus geantwortet werden, wenn gefragt wird, was passiert, wenn ein Zug auf einen Spielstatus angewendet wird. Es könnte auch einen Anfangszustand des Spiels liefern, wenn nach einem anfänglichen Layout gefragt wird.

Der Controller muss den Spielstatus und das Regelwerk und möglicherweise einige andere Objekte des Spielmodells kennen, aber er sollte nicht mit den Details herumspielen müssen.

KOMME AUS
quelle
4
Genau mein Denken. Das OP vermischt zu viele Daten und Prozeduren in denselben Klassen. Es ist besser, diese mehr aufzuteilen. Dies ist ein gutes Gespräch zu diesem Thema. Übrigens, wenn ich "Sicht auf einen Spielstatus" lese, denke ich "Argument für die Funktion". +100 wenn ich könnte.
jpmc26
5

Ich denke, das Problem dabei ist, dass Sie nicht klar beschrieben haben, welche Aufgaben von welchen Klassen zu erledigen sind. Ich werde beschreiben, was meiner Meinung nach eine gute Beschreibung dessen ist, was jede Klasse tun sollte, und dann ein Beispiel für allgemeinen Code geben, der die Ideen veranschaulicht. Wir werden sehen, dass der Code weniger gekoppelt ist und daher keine Zirkelverweise enthält.

Beginnen wir mit der Beschreibung der einzelnen Klassen.

Die GameStateKlasse sollte nur Informationen über den aktuellen Stand des Spiels enthalten. Es sollte keine Informationen darüber enthalten, in welchem ​​Zustand sich das Spiel in der Vergangenheit befindet oder welche zukünftigen Züge möglich sind. Es sollte nur Informationen darüber enthalten, welche Figuren sich auf welchen Feldern im Schach befinden oder wie viele und welche Arten von Dame sich auf welchen Punkten im Backgammon befinden. Das GameStatemuss einige zusätzliche Informationen enthalten, wie zum Beispiel Informationen über das Rochieren im Schach oder über den Verdopplungswürfel im Backgammon.

Der MoveUnterricht ist etwas knifflig. Ich würde sagen, dass ich einen Zug angeben kann, der gespielt werden soll, indem ich festlege, welcher Zug aus dem Spiel GameStateresultiert. Sie können sich also vorstellen, dass ein Umzug nur als GameState. In go (zum Beispiel) könnte man sich jedoch vorstellen, dass es viel einfacher ist, einen Zug zu bestimmen, indem man einen einzelnen Punkt auf dem Brett angibt. Wir möchten, dass unsere MoveKlasse flexibel genug ist, um beide Fälle zu behandeln. Daher wird die MoveKlasse tatsächlich eine Schnittstelle mit einer Methode sein, die einen Vorversatz ausführt GameStateund einen neuen Nachversatz zurückgibt GameState.

Jetzt ist die RuleBookKlasse dafür verantwortlich, alles über die Regeln zu wissen. Dies kann in drei Dinge unterteilt werden. Es muss wissen, was die Initiale GameStateist, es muss wissen, welche Züge legal sind, und es muss in der Lage sein zu erkennen, ob einer der Spieler gewonnen hat.

Sie könnten auch eine GameHistoryKlasse bilden, um alle Bewegungen zu verfolgen, die gemacht worden sind und alle GameStates, die geschehen sind. Eine neue Klasse ist notwendig, weil wir entschieden haben, dass eine einzelne GameStatenicht dafür verantwortlich sein sollte, alle GameStates zu kennen, die davor standen.

Dies schließt die Klassen / Interfaces ab, die ich diskutieren werde. Du hast auch eine BoardKlasse. Ich denke jedoch, dass Boards in verschiedenen Spielen so unterschiedlich sind, dass es schwer zu erkennen ist, was generell mit Boards gemacht werden kann. Jetzt werde ich generische Interfaces geben und generische Klassen implementieren.

Erstens ist GameState. Da diese Klasse vollständig vom jeweiligen Spiel abhängig ist, gibt es keine generische GamestateSchnittstelle oder Klasse.

Weiter ist Move. Wie ich bereits sagte, kann dies mit einer Schnittstelle dargestellt werden, die über eine einzige Methode verfügt, die einen Zustand vor dem Verschieben annimmt und einen Zustand nach dem Verschieben erzeugt. Hier ist der Code für diese Schnittstelle:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Beachten Sie, dass es einen Typparameter gibt. Dies liegt zum Beispiel daran, dass ein ChessMoveWille etwas über die Einzelheiten des Vorzugs wissen muss ChessGameState. So zum Beispiel der Klasse Erklärung ChessMovewäre

class ChessMove extends Move<ChessGameState>,

wo du schon eine ChessGameStateKlasse definiert hättest .

Als nächstes werde ich die generische RuleBookKlasse diskutieren . Hier ist der Code:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

Auch hier gibt es einen Typparameter für die GameStateKlasse. Da die RuleBookwissen soll, wie der Ausgangszustand ist, haben wir eine Methode entwickelt, um den Ausgangszustand anzugeben. Da RuleBookwissen soll, welche Züge legal sind, haben wir Methoden, um zu testen, ob ein Zug in einem bestimmten Staat legal ist, und um eine Liste der legalen Züge für einen bestimmten Staat zu erstellen. Schließlich gibt es eine Methode zur Auswertung der GameState. Beachten Sie RuleBook, dass der Spieler nur dafür verantwortlich sein sollte, zu beschreiben, ob der eine oder andere Spieler bereits gewonnen hat, aber nicht, wer sich in der Mitte eines Spiels in einer besseren Position befindet. Zu entscheiden, wer in einer besseren Position ist, ist eine komplizierte Sache, die in eine eigene Klasse verschoben werden sollte. Daher ist die StateEvaluationKlasse eigentlich nur eine einfache Aufzählung, die wie folgt lautet:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Zuletzt beschreiben wir die GameHistoryKlasse. Diese Klasse ist dafür verantwortlich, sich alle im Spiel erreichten Positionen sowie die gespielten Züge zu merken. Die Hauptsache, die es tun sollte, ist, ein Video aufzunehmen, Movewie es abgespielt wurde. Sie können auch Funktionen zum Rückgängigmachen von Moves hinzufügen . Ich habe eine Implementierung unten.

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

Schließlich könnten wir uns vorstellen, eine GameKlasse zu bilden, um alles zusammenzubinden. Diese GameKlasse soll Methoden aufzeigen, die es den Leuten ermöglichen, zu sehen, was der Strom GameStateist, zu sehen, wer, wenn jemand einen hat, welche Züge gespielt werden können, und einen Zug zu spielen. Ich habe eine Implementierung unten

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

Beachten Sie in dieser Klasse, dass der RuleBooknicht dafür verantwortlich ist, zu wissen, was der Strom GameStateist. Das ist der GameHistoryJob des. Der Gamefragt also nach dem GameHistoryaktuellen Stand und gibt diese Information an, RuleBookwann der GameBedarf besteht, zu sagen, was die legalen Schritte sind oder ob jemand gewonnen hat.

Der Sinn dieser Antwort ist jedenfalls, dass Sie, sobald Sie eine vernünftige Entscheidung getroffen haben, wofür jede Klasse verantwortlich ist, jede Klasse auf eine kleine Anzahl von Verantwortlichkeiten fokussieren und jede Verantwortung einer eindeutigen Klasse zuweisen, die Klassen neigen dazu, sich zu entkoppeln, und alles wird leicht zu codieren. Hoffentlich geht das aus den Codebeispielen hervor, die ich gegeben habe.

Brian Moths
quelle
3

Zirkuläre Verweise weisen meiner Erfahrung nach im Allgemeinen darauf hin, dass Ihr Design nicht gut durchdacht ist.

In Ihrem Entwurf verstehe ich nicht, warum RuleBook über den Staat "Bescheid wissen" muss. Sicher, es könnte einen Zustand als Parameter für eine Methode erhalten, aber warum sollte es einen Verweis auf einen Zustand kennen müssen (dh als Instanzvariable halten)? Das ergibt für mich keinen Sinn. Ein Regelbuch muss den Status eines bestimmten Spiels nicht "kennen", um seine Aufgabe zu erfüllen. Die Spielregeln ändern sich nicht in Abhängigkeit vom aktuellen Status des Spiels. Entweder haben Sie es falsch entworfen oder Sie haben es richtig entworfen, erklären es aber falsch.

Mehrdad
quelle
+1. Sie kaufen ein physisches Brettspiel, Sie erhalten ein Regelbuch, das die Regeln ohne Zustand beschreiben kann.
unperson325680
1

Die zirkuläre Abhängigkeit ist nicht unbedingt ein technisches Problem, sollte jedoch als Code-Geruch betrachtet werden, der in der Regel eine Verletzung des Single-Responsibility-Prinzips darstellt .

Ihre zirkuläre Abhängigkeit beruht auf der Tatsache, dass Sie versuchen, zu viel aus Ihrem StateObjekt herauszuholen.

Jedes zustandsbehaftete Objekt sollte nur Methoden bereitstellen, die in direktem Zusammenhang mit der Verwaltung dieses lokalen Zustands stehen. Wenn es mehr als die grundlegendste Logik erfordert, sollte es wahrscheinlich in ein größeres Muster aufgeteilt werden. Einige Leute haben unterschiedliche Meinungen dazu, aber als Faustregel gilt: Wenn Sie mehr tun als nur Daten abrufen und einstellen, tun Sie zu viel.

In diesem Fall ist es besser, wenn Sie eine haben StateFactory, die etwas über eine wissen könnte Rulebook. Du hättest wahrscheinlich eine andere Controller-Klasse, mit der du StateFactoryein neues Spiel erstellst. Statesollte auf keinen Fall wissen Rulebook. RulebookMöglicherweise wissen Sie von einer Stateabhängig von der Implementierung Ihrer Regeln.

00500005
quelle
0

Muss ein Regelbuchobjekt an einen bestimmten Spielstatus gebunden sein, oder ist es sinnvoller, ein Regelbuchobjekt mit einer Methode zu haben, die in einem bestimmten Spielstatus angibt, welche Züge in diesem Status verfügbar sind (und nachdem ich das berichtet habe, erinnerst du dich an nichts über den fraglichen Staat? Wenn das Objekt, das nach verfügbaren Zügen gefragt wird, keine Erinnerung an den Spielstatus behält, muss es keine Referenz beibehalten.

In einigen Fällen kann es von Vorteil sein, wenn das regelauswertende Objekt den Status beibehält. Wenn Sie der Meinung sind, dass eine solche Situation eintreten könnte, würde ich vorschlagen, eine Klasse "referee" hinzuzufügen und das Regelbuch eine Methode "createReferee" bereitstellen zu lassen. Im Gegensatz zum Regelwerk, das sich nicht darum kümmert, ob es sich um ein oder fünfzig Spiele handelt, würde ein Schiedsrichterobjekt erwarten, ein Spiel zu regieren. Es ist nicht zu erwarten, dass der gesamte Status in Bezug auf das Spiel, das es amtiert, zusammengefasst wird, es kann jedoch alle Informationen über das Spiel zwischengespeichert werden, die es als nützlich erachtet. Wenn ein Spiel die Funktion "Rückgängig" unterstützt, kann es hilfreich sein, wenn der Schiedsrichter ein Mittel zum Erstellen eines "Schnappschuss" -Objekts einschließt, das zusammen mit früheren Spielzuständen gespeichert werden kann. dieses Objekt sollte,

Wenn eine Kopplung zwischen den Aspekten der Regelverarbeitung und der Spielzustandsverarbeitung des Codes erforderlich sein könnte, wird die Verwendung eines Schiedsrichterobjekts es ermöglichen, eine solche Kopplung aus dem Hauptregelwerk und den Spielzustandsklassen herauszuhalten. Es kann auch möglich sein, dass neue Regeln Aspekte des Spielzustands berücksichtigen, die von der Spielzustandsklasse nicht als relevant erachtet werden (z. B. wenn eine Regel hinzugefügt wurde, die besagt, dass "Objekt X kein Y ausführen kann, wenn es sich jemals an Position Z befunden hat ", der Schiedsrichter könnte geändert werden, um zu verfolgen, welche Objekte sich an Ort Z befunden haben, ohne die Spielzustandsklasse ändern zu müssen).

Superkatze
quelle
-2

Der richtige Umgang damit ist die Verwendung von Schnittstellen. Anstatt zwei Klassen voneinander zu kennen, muss jede Klasse eine Schnittstelle implementieren und auf die der anderen Klasse verweisen. Angenommen, Sie haben Klasse A und Klasse B, die sich gegenseitig referenzieren müssen. Lassen Sie die Klasse A die Schnittstelle A und die Klasse B die Schnittstelle B implementieren, dann können Sie die Schnittstelle B aus Klasse A und die Schnittstelle A aus Klasse B referenzieren. Klasse A kann sich dann in einem eigenen Projekt befinden, ebenso wie Klasse B. Die Schnittstellen befinden sich in einem separaten Projekt worauf sich beide anderen Projekte beziehen.

Peter
quelle
2
dies scheint nur wiederholen Sie die Punkte gemacht und erklärt in einer vor Antwort geschrieben wenige Stunden vor diesem
gnat