it-swarm.com.de

Was ist der richtige Weg, um diese reale Aktivität zu modellieren, für die in OOP Zirkelverweise erforderlich zu sein scheinen?

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 es wissen müssen über einander.

Das Projekt ist ein generisches Modell für ein Brettspiel. Die Grundklassen sind nicht spezifisch, werden jedoch erweitert, um sich mit Besonderheiten von Schach, Backgammon und anderen Spielen zu befassen. Ich habe dies vor 11 Jahren als Applet mit einem halben Dutzend verschiedener Spiele codiert, 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 abgefragt werden kann, z. B. nach dem anfänglichen Layout des Bretts, anderen Informationen zum anfänglichen Spielstatus, z. B. wer sich zuerst bewegt, den verfügbaren Zügen, was mit dem Spielstatus nach einem vorgeschlagenen Zug passiert, und einer Bewertung von eine aktuelle oder vorgeschlagene Vorstandsposition.

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

  • MoveList: Eine Liste der Moves. Dies dient zwei Zwecken: einer Auswahl von Zügen, die zu einem bestimmten Zeitpunkt verfügbar sind, oder einer Liste von Zügen, die im Spiel ausgeführt wurden. Es könnte in zwei nahezu identische Klassen aufgeteilt werden, aber das ist für die Frage, die ich stelle, nicht relevant und kann es weiter erschweren.

  • Zug: ein einziger Zug. Es enthält alles über den Zug als 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 aufzeichnen, ob der König und die Türme jedes Spielers bewegt wurden.

Zirkelverweise gibt es zuhauf: 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 abfragen und nach den Nebenwirkungen, die mit einem Zug einmal einhergehen es wird gemacht (zB wer bewegt sich als nächstes).

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

Wie kann man das also richtig organisieren? Sollte ich RuleBook in BigClassThatDoesAlmostEverythingInTheGame verwandeln, um Zirkelverweise zu vermeiden und den Versuch aufzugeben, 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, wobei mein reales Modell beibehalten wird? Oder fehlt mir eine offensichtlich gültige Struktur?

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

24
Damian Walker

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

Der Garbage Collector von Java basiert nicht auf Referenzzählungstechniken. Zirkelverweise verursachen in Java keinerlei Probleme. Zeitaufwand für das Eliminieren vollkommen natürlicher Zirkelverweise in Java ist Zeitverschwendung.

Ich habe dies [...] codiert, 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, [...]

Nicht nötig. Wenn Sie nur alle Quelldateien gleichzeitig kompilieren (z. B. javac *.Java) löst der Compiler alle Forward-Referenzen ohne Probleme auf.

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

Ja. Es wird erwartet, dass Anwendungsklassen voneinander abhängig sind. Das Kompilieren aller Java Quelldateien, die gleichzeitig zum selben Paket gehören, ist kein kluger Hack, sondern genau so Java ist soll funktionieren.

47
Atsby

Zugegeben, zirkuläre Abhängigkeiten sind aus gestalterischer Sicht eine fragwürdige Praxis, aber sie sind nicht verboten, und aus rein technischer Sicht sind sie nicht einmal notwendigerweise problematisch, wie Sie sie zu betrachten scheinen be: Sie sind in den meisten Szenarien vollkommen legal, in einigen Situationen unvermeidlich und in einigen seltenen Fällen können sie sogar als nützlich angesehen werden.

Tatsächlich gibt es nur sehr wenige Szenarien, in denen der Java -Compiler eine zirkuläre Abhängigkeit verweigert. (Hinweis: Es kann mehr geben, ich kann mir derzeit nur Folgendes vorstellen.)

  1. In der Vererbung: Sie können keine Klasse A haben, die Klasse B erweitert, was 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 ergeben würde.

  2. Unter methodenlokalen Klassen: Klassen, die innerhalb einer Methode deklariert wurden, verweisen möglicherweise nicht kreisförmig aufeinander. Dies ist wahrscheinlich nichts anderes als eine Einschränkung des Competers Java), 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 fließen 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 können.)

Daher ist es wichtig zu erkennen und aus dem Weg zu räumen, dass das Bestreben, zirkuläre Abhängigkeiten zu minimieren, ein Streben nach Designreinheit und kein Streben nach technischer Korrektheit ist.

Soweit ich weiß, gibt es keinen reduktionistischen Ansatz, um zirkuläre Abhängigkeiten zu beseitigen, was bedeutet, dass es kein Rezept gibt, das nur aus vorgegebenen "einfachen" Schritten besteht, um ein System mit zirkulären Referenzen zu erstellen, sie nacheinander anzuwenden und zu beenden mit einem System frei von Zirkelverweisen. Sie müssen Ihren Verstand zur Arbeit bringen und Refactoring-Schritte ausfü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, vielleicht "Game" oder "GameLogic" genannt, die alle anderen Entitäten kennt (ohne dass eine der anderen Entitäten es weiß). ), damit sich die anderen Entitäten nicht kennen müssen.

Zum Beispiel erscheint es mir unvernünftig, dass Ihre RuleBook-Entität irgendetwas über die GameState-Entität wissen muss, da ein Regelbuch etwas ist, das wir zum Spielen konsultieren, es ist nichts, was aktiv am Spielen teilnimmt. Es ist also diese neue "Spiel" -Entität, die sowohl das Regelbuch als auch den Spielstatus konsultieren muss, um festzustellen, 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 spielunabhängige Codieren der "Spiel" -Entität wird sehr schwierig sein, sodass Sie höchstwahrscheinlich nicht nur eins, sondern zwei haben werden Entitäten, für die für jeden Spieltyp maßgeschneiderte Implementierungen erforderlich sind: die Entität "RuleBook" und "Game". Was wiederum den Zweck einer "Regelbuch" -Entität zunichte macht. 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 edel, aber vielleicht schlecht durchdacht war. Wenn ich in Ihren Schuhen steckte, 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 all diese Spiele zu verwenden, und dann hätte ich die Implementierung der verschiedenen Spiele zugelassen stark variieren, sehr wenig Code gemeinsam haben, um maximale Flexibilität bei der Implementierung jedes Spiels zu gewährleisten.

22
Mike Nakis

Die Spieltheorie behandelt Spiele als eine Liste vorheriger Züge (Werttypen einschließlich derjenigen, die sie gespielt haben) und als eine Funktion ValidMoves (vorherigeMoves).

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

die Benutzeroberfläche kann dann Standard sein OO Zeug mit einer Einwegreferenz zur Logik


Update, um Kommentare zu verdichten

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

die Liste der Züge definiert den vollständigen Status des Spiels viel besser als ein Bild des Bretts.

Angenommen, wir erstellen Objekte für Board, Piece, Move usw. und Methoden wie Piece.GetValidMoves ()

zuerst sehen wir, dass wir eine Stückreferenz auf der Tafel haben müssen, aber dann ziehen wir die Rochade in Betracht. Dies können Sie nur tun, wenn Sie Ihren König oder Turm noch nicht bewegt haben. Wir brauchen also eine MovedAlready-Flagge für den König und die Türme. Ebenso können Bauern beim ersten Zug 2 Felder bewegen.

Dann sehen wir, dass bei der Rochade die gültige Bewegung des Königs von der Existenz und dem Zustand des Turms abhängt. Daher muss das Brett Teile darauf haben und auf diese Teile verweisen. Wir gehen auf Ihr Problem mit Rundschreiben ein.

Wenn wir jedoch Move als unveränderliche Struktur und Spielstatus als Liste der vorherigen Moves definieren, verschwinden diese Probleme. Um zu sehen, ob die Rochade gültig ist, können wir die Umzugsliste der Existenz von Burg- und Königszügen überprüfen. Um zu sehen, ob der Bauer en-passent werden kann, können wir überprüfen, ob der andere Bauer zuvor einen doppelten Zug gemacht hat. Es werden keine Referenzen außer Regeln -> Verschieben benötigt

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

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

Wenn das Spielbrett selbst in Ihrem Spiel 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 die Gesamtstruktur Ihrer Regelengine zu ändern oder auf ein BoardSetup Objekt ähnlicher Art verweisen zu müssen

10
Ewan

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önnte sich RuleBook auf State beziehen, das sich dann auf ein InitialPositionProvider bezieht (was eine von RuleBook implementierte Schnittstelle wäre). Dies erleichtert auch das Testen, da Sie dann ein State erstellen können, das zu Testzwecken eine andere (vermutlich einfachere) Anfangsposition verwendet.

8
Jules

Ich glaube, die Zirkelverweise und das Gottobjekt in Ihrem Fall könnten leicht entfernt werden, indem die Kontrolle des Spielflusses von den Zustands- 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 tatsächliche Statusänderungen handhabt, anstatt dem Regelbuch 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 einfach zu handhabende (erstellte, geprüfte, geänderte, beibehaltene, protokollierte, kopierte, zwischengespeicherte usw.) und effiziente Spielstatusobjekte für den Rest der Anwendung bereitstellen.

Das Regelbuch sollte nichts über ein laufendes Spiel wissen oder damit herumspielen müssen. Es sollte nur eine Ansicht eines Spielstatus erforderlich sein, um feststellen zu können, welche Züge legal 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 anfänglichen Spielstatus liefern, wenn nach einem anfänglichen Layout gefragt wird.

Der Controller muss die Spielzustände und das Regelwerk sowie möglicherweise einige andere Objekte des Spielmodells kennen, sollte sich jedoch nicht mit den Details herumschlagen müssen.

6
COME FROM

Ich denke, das Problem hier 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 generischen 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 Klasse GameState sollte nur Informationen über den aktuellen Status des Spiels enthalten. Es sollte keine Informationen darüber enthalten, was die vergangenen Zustände des Spiels sind 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 Kontrolleuren sich auf welchen Punkten im Backgammon befinden. Das GameState muss einige zusätzliche Informationen enthalten, z. B. Informationen über das Rochieren im Schach oder über den Verdopplungswürfel im Backgammon.

Die Klasse Move ist etwas knifflig. Ich würde sagen, dass ich einen zu spielenden Zug angeben kann, indem ich das GameState spezifiziere, das sich aus dem Spielen des Zuges ergibt. Sie können sich also vorstellen, dass ein Zug nur als GameState implementiert werden kann. In go (zum Beispiel) können Sie sich jedoch vorstellen, dass es viel einfacher ist, einen Zug anzugeben, indem Sie einen einzelnen Punkt auf dem Brett angeben. Wir möchten, dass unsere Klasse Move flexibel genug ist, um beide Fälle zu behandeln. Daher wird die Klasse Move tatsächlich eine Schnittstelle mit einer Methode sein, die eine Vorverschiebung GameState verwendet und eine neue Nachverschiebung GameState zurückgibt.

Jetzt ist die Klasse RuleBook dafür verantwortlich, alles über die Regeln zu wissen. Dies kann in drei Dinge unterteilt werden. Es muss wissen, was das anfängliche GameState ist, 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önnen auch eine GameHistory -Klasse erstellen, um alle durchgeführten Züge und alle aufgetretenen GameStates zu verfolgen. Eine neue Klasse ist notwendig, da wir entschieden haben, dass ein einzelnes GameState nicht dafür verantwortlich sein sollte, alle GameStates zu kennen, die davor standen.

Damit sind die Klassen/Schnittstellen abgeschlossen, die ich diskutieren werde. Sie haben auch eine Board Klasse. Aber ich denke, Bretter in verschiedenen Spielen sind so unterschiedlich, dass es schwer zu erkennen ist, was generisch mit Brettern gemacht werden könnte. Jetzt werde ich generische Schnittstellen angeben und generische Klassen implementieren.

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

Als nächstes kommt Move. Wie gesagt, dies kann mit einer Schnittstelle dargestellt werden, die über eine einzige Methode verfügt, die einen Status vor dem Verschieben annimmt und einen Status 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 beispielsweise daran, dass ein ChessMove die Einzelheiten des Vorzugs ChessGameState kennen muss. So wäre beispielsweise die Klassendeklaration von ChessMove

class ChessMove extends Move<ChessGameState>,

wo Sie bereits eine ChessGameState Klasse definiert hätten.

Als nächstes werde ich die generische RuleBook Klasse 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);

}

Wieder gibt es einen Typparameter für die Klasse GameState. Da RuleBook wissen soll, wie der Anfangszustand ist, haben wir eine Methode festgelegt, um den Anfangszustand anzugeben. Da RuleBook wissen 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 legaler Züge für einen bestimmten Staat zu erstellen. Schließlich gibt es eine Methode, um das GameState auszuwerten. Beachten Sie, dass RuleBook nur dafür verantwortlich sein sollte, zu beschreiben, ob der eine oder andere Spieler bereits gewonnen hat, aber nicht, wer sich mitten in einem Spiel 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 Klasse StateEvaluation eigentlich nur eine einfache Aufzählung, die wie folgt angegeben wird:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Zuletzt beschreiben wir die Klasse GameHistory. Diese Klasse ist dafür verantwortlich, sich alle Positionen zu merken, die im Spiel erreicht wurden, sowie die Züge, die gespielt wurden. Die Hauptsache, die es tun sollte, ist, ein Move wie gespielt aufzunehmen. Sie können auch Funktionen zum Rückgängigmachen von Moves hinzufügen. Ich habe unten eine Implementierung.

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 Game Klasse zu erstellen, um alles zusammenzubinden. Diese Game -Klasse soll Methoden offenlegen, die es den Menschen ermöglichen, zu sehen, was das aktuelle GameState ist, zu sehen, wer, wenn jemand eine hat, welche Züge gespielt werden können, und a zu spielen Bewegung. Ich habe unten eine Implementierung

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 RuleBook nicht dafür verantwortlich ist, das aktuelle GameState zu kennen. Das ist der Job von GameHistory. Also fragt das Game das GameHistory nach dem aktuellen Status und gibt diese Informationen an das RuleBook weiter, wenn das Game sagen muss, was die legalen Bewegungen sind oder wenn jemand gewonnen hat.

Der Punkt dieser Antwort ist jedenfalls, dass, sobald Sie eine vernünftige Bestimmung getroffen haben, wofür jede Klasse verantwortlich ist, und Sie jede Klasse auf eine kleine Anzahl von Verantwortlichkeiten konzentrieren und jede Verantwortung einer eindeutigen Klasse zuweisen, dann den Klassen neigen dazu, entkoppelt zu werden, und alles wird einfach zu codieren. Hoffentlich geht das aus den Codebeispielen hervor, die ich gegeben habe.

5
Brian Moths

Nach meiner Erfahrung weisen Zirkelverweise 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. Es könnte einen Status als Parameter für eine Methode erhalten, sicher, aber warum sollte es wissen (d. H. Als Instanzvariable halten) einen Verweis auf einen Status erhalten müssen? Das ergibt für mich keinen Sinn. Ein Regelbuch muss nicht über den Status eines bestimmten Spiels "Bescheid wissen", um seine Aufgabe zu erfüllen. Die Spielregeln ändern sich je nach aktuellem Spielstatus nicht. Entweder haben Sie es falsch entworfen oder Sie haben es richtig entworfen, erklären es aber falsch.

3
user541686

Die zirkuläre Abhängigkeit ist nicht unbedingt ein technisches Problem, sollte jedoch als Codegeruch betrachtet werden, der normalerweise eine Verletzung des Single Responsibility Principle darstellt.

Ihre zirkuläre Abhängigkeit ergibt sich aus der Tatsache, dass Sie versuchen, zu viel aus Ihrem State -Objekt herauszuholen.

Jedes statusbehaftete Objekt sollte nur Methoden bereitstellen, die sich direkt auf die Verwaltung dieses lokalen Status beziehen. 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 als nur Getter und Setter für Daten tun, tun Sie zu viel.

In diesem Fall ist es besser, ein StateFactory zu haben, das über ein Rulebook Bescheid wissen könnte. Sie hätten wahrscheinlich eine andere Controller-Klasse, die Ihr StateFactory verwendet, um ein neues Spiel zu erstellen. State sollte definitiv nichts über Rulebook wissen. Rulebook kennt möglicherweise ein State, abhängig von der Implementierung Ihrer Regeln.

1
00500005

Muss ein Regelbuchobjekt an einen bestimmten Spielstatus gebunden werden, oder ist es sinnvoller, ein Regelbuchobjekt mit einer Methode zu haben, die bei gegebenem Spielstatus meldet, welche Züge aus diesem Status verfügbar sind (und, Erinnern Sie sich an nichts über den fraglichen Staat, nachdem Sie dies gemeldet haben? Sofern nicht etwas gewonnen werden kann, indem das Objekt, das nach verfügbaren Zügen gefragt wird, eine Erinnerung an den Spielstatus behält, muss es keine Referenz beibehalten.

In einigen Fällen ist es möglich, dass der Status des Objekts, das die Regeln auswertet, beibehalten wird. Wenn Sie glauben, dass eine solche Situation auftreten könnte, würde ich vorschlagen, eine "Schiedsrichter" -Klasse hinzuzufügen und das Regelbuch eine "createReferee" -Methode bereitstellen zu lassen. Im Gegensatz zum Regelbuch, das sich nicht darum kümmert, ob es um ein oder fünfzig Spiele geht, würde ein Schiedsrichterobjekt erwarten, ein Spiel zu leiten. Es ist nicht zu erwarten, dass der gesamte Status in Bezug auf das Spiel, das es leitet, zusammengefasst wird, aber es könnte alle Informationen über das Spiel zwischenspeichern, 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 enthält, das zusammen mit früheren Spielzuständen gespeichert werden kann. Dieses Objekt sollte bei einer Kopie des Spielstatus, als es erstellt wurde, in der Lage sein, einen neuen Schiedsrichter zu erstellen, dessen Status mit dem Status übereinstimmen sollte, den der ursprüngliche Schiedsrichter hatte, als der Schnappschuss erstellt wurde.

Wenn eine gewisse Kopplung zwischen den Aspekten der Regelverarbeitung und der Verarbeitung des Spielzustands des Codes erforderlich sein könnte, ermöglicht die Verwendung eines Schiedsrichterobjekts die Beibehaltung einer solchen Kopplung out des Hauptregelwerks und des Spielzustands Klassen. Es kann auch möglich sein, dass neue Regeln Aspekte des Spielzustands berücksichtigen, die die Spielstatusklasse nicht als relevant erachtet (z. B. wenn eine Regel hinzugefügt wurde, die besagt: "Objekt X kann Y nicht ausführen, wenn es jemals an Position Z war." "könnte der Schiedsrichter geändert werden, um zu verfolgen, welche Objekte sich an Position Z befunden haben, ohne die Spielstatusklasse ändern zu müssen).

0
supercat