Quelle est la bonne façon de modéliser cette activité du monde réel qui semble avoir besoin de références circulaires dans la POO?

24

J'ai été aux prises avec un problème dans un projet Java sur les références circulaires. J'essaie de modéliser une situation réelle dans laquelle il semble que les objets en question sont interdépendants et doivent se connaître les uns les autres.

Le projet est un modèle générique de jeu de société. Les classes de base ne sont pas spécifiques, mais sont étendues pour traiter des spécificités des échecs, du backgammon et d'autres jeux. J'ai codé cela comme une applet il y a 11 ans avec une demi-douzaine de jeux différents, mais le problème est qu'il est plein de références circulaires. Je l'ai implémenté à l'époque en bourrant toutes les classes entrelacées dans un seul fichier source, mais j'ai l'idée que c'est une mauvaise forme en Java. Maintenant, je veux implémenter une chose similaire à une application Android, et je veux faire les choses correctement.

Les cours sont:

  • RuleBook: un objet qui peut être interrogé pour des choses telles que la disposition initiale du plateau, d'autres informations initiales sur l'état du jeu comme qui bouge en premier, les mouvements disponibles, ce qui arrive à l'état du jeu après un mouvement proposé, et une évaluation de un poste actuel ou proposé au conseil.

  • Plateau: une représentation simple d'un plateau de jeu, qui peut être chargé de refléter un mouvement.

  • MoveList: une liste de mouvements. Il s'agit d'un double objectif: un choix de mouvements disponibles à un moment donné, ou une liste de mouvements qui ont été effectués dans le jeu. Il pourrait être divisé en deux classes presque identiques, mais cela n'est pas pertinent pour la question que je pose et pourrait compliquer davantage.

  • Déplacer: un seul coup. Il comprend tout ce qui concerne le mouvement sous forme de liste d'atomes: prenez un morceau d'ici, posez-le là-bas, retirez un morceau capturé de là.

  • État: toutes les informations d'état d'un jeu en cours. Non seulement la position du conseil d'administration, mais une MoveList et d'autres informations d'état telles que qui doit se déplacer maintenant. Aux échecs, on enregistre si le roi et les tours de chaque joueur ont été déplacés.

Les références circulaires abondent, par exemple: le RuleBook doit connaître l'état du jeu pour déterminer quels mouvements sont disponibles à un moment donné, mais l'État du jeu doit interroger le RuleBook pour la disposition de départ initiale et pour quels effets secondaires accompagnent un coup une fois. c'est fait (par exemple, qui bouge ensuite).

J'ai essayé d'organiser le nouvel ensemble de classes de manière hiérarchique, avec RuleBook en haut car il doit tout savoir. Mais cela se traduit par le fait de devoir déplacer de nombreuses méthodes dans la classe RuleBook (comme effectuer un mouvement), ce qui la rend monolithique et pas particulièrement représentative de ce que devrait être un RuleBook.

Alors, quelle est la bonne façon d'organiser cela? Dois-je transformer RuleBook en BigClassThatDoesAlmostEverythingInTheGame pour éviter les références circulaires, abandonnant la tentative de modéliser le jeu réel avec précision? Ou devrais-je m'en tenir aux classes interdépendantes et inciter le compilateur à les compiler d'une manière ou d'une autre, en conservant mon modèle du monde réel? Ou y a-t-il une structure valide évidente qui me manque?

Merci pour toute l'aide que vous pouvez nous apporter!

Damian Walker
la source
7
Que se passe-t-il si le RuleBookprend par exemple le Statecomme argument et retourne le valide MoveList, c'est-à-dire "voici où nous en sommes maintenant, que peut-on faire ensuite?"
jonrsharpe
Ce que @jonrsharpe a dit. Lorsque vous jouez à un vrai jeu de société, le livre de règles ne connaît pas non plus les jeux réellement joués. J'introduirais probablement même une autre classe pour calculer les mouvements, mais cela pourrait dépendre de la taille de cette classe RuleBook.
Sebastiaan van den Broek
4
Éviter l'objet divin (BigClassThatDoesAlmostEverythingInTheGame) est beaucoup plus important que d'éviter les références circulaires.
user281377
2
@ user281377, ce ne sont pas nécessairement des objectifs mutuellement exclusifs!
jonrsharpe
1
Pouvez-vous montrer les tentatives de modélisation? Un diagramme par exemple?
Utilisateur

Réponses:

47

J'ai été aux prises avec un problème dans un projet Java sur les références circulaires.

Le garbage collector de Java ne repose pas sur des techniques de comptage de références. Les références circulaires ne causent aucun type de problème en Java. Le temps passé à éliminer les références circulaires parfaitement naturelles en Java est une perte de temps.

J'ai codé cela [...] mais le problème est qu'il est plein de références circulaires. Je l'ai implémenté à l'époque en bourrant toutes les classes entrelacées dans un seul fichier source , [...]

Pas nécessaire. Si vous compilez simplement tous les fichiers source à la fois (par exemple, javac *.java), le compilateur résoudra sans problème toutes les références avancées.

Ou devrais-je m'en tenir aux classes interdépendantes et amadouer le compilateur pour les compiler en quelque sorte, [...]

Oui. Les classes d'application devraient être interdépendantes. Compiler tous les fichiers source Java qui appartiennent au même package à la fois n'est pas un hack intelligent, c'est précisément la façon dont Java est censé fonctionner.

Atsby
la source
24
"Les références circulaires ne posent aucun problème en Java." En termes de compilation, c'est vrai. Cependant, les références circulaires sont considérées comme une mauvaise conception .
Chop
22
Les références circulaires sont parfaitement naturelles dans de nombreuses situations, c'est pourquoi Java et d'autres langages modernes utilisent un garbage collector sophistiqué au lieu d'un simple compteur de références.
user281377
3
Java est capable de résoudre des références circulaires est génial, et il est certainement vrai qu'elles sont naturelles dans de nombreuses situations. Mais OP a présenté une situation spécifique , et cela doit être pris en considération. Le code de spaghetti enchevêtré n'est probablement pas la meilleure façon de gérer ce problème.
Matthew Read
3
Veuillez ne pas diffuser de FUD non corroboré sur les langages de programmation non liés. Python prend en charge GC de cycles de référence depuis des âges ( docs , également sur SO: ici et ici ).
Christian Aichinger
2
À mon humble avis, cette réponse n'est que médiocre, car il n'y a pas un mot sur les références circulaires utiles par exemple du PO.
Doc Brown
22

Certes, les dépendances circulaires sont une pratique discutable du point de vue de la conception, mais elles ne sont pas interdites et, d'un point de vue purement technique, elles ne sont même pas nécessairement problématiques , comme vous semblez les considérer: elles sont parfaitement légales dans la plupart des scénarios, ils sont inévitables dans certaines situations, et en de rares occasions, ils peuvent même être considérés comme une chose utile à avoir.

En fait, il y a très peu de scénarios où le compilateur java refusera une dépendance circulaire. (Remarque: il peut y en avoir plus, je ne peux penser qu'à ce qui suit en ce moment.)

  1. En héritage: vous ne pouvez pas avoir une classe B étendue de classe A qui à son tour étend la classe A, et il est parfaitement raisonnable que vous ne puissiez pas avoir cela, car l'alternative n'aurait aucun sens d'un point de vue logique.

  2. Parmi les classes méthode-locales: les classes déclarées dans une méthode peuvent ne pas se référencer de façon circulaire. Ce n'est probablement rien d'autre qu'une limitation du compilateur java, peut-être parce que la capacité de faire une telle chose n'est pas assez utile pour justifier la complexité supplémentaire qui devrait aller dans le compilateur pour le supporter. (La plupart des programmeurs Java ne sont même pas conscients du fait que vous pouvez déclarer une classe dans une méthode, encore moins déclarer plusieurs classes, puis que ces classes se référencent de manière circulaire.)

Il est donc important de comprendre et de ne pas gêner que la quête pour minimiser les dépendances circulaires est une quête de pureté de conception, pas une quête de correction technique.

Autant que je sache, il n'existe pas d'approche réductionniste pour éliminer les dépendances circulaires, ce qui signifie qu'il n'y a pas de recette composée uniquement d'étapes simples et prédéfinies pour prendre un système avec des références circulaires, les appliquer l'une après l'autre et se terminer avec un système sans références circulaires. Vous devez mettre votre esprit au travail et effectuer des étapes de refactoring qui dépendent de la nature de votre conception.

Dans la situation particulière que vous avez sous la main, il me semble que vous avez besoin d'une nouvelle entité, peut-être appelée "Game" ou "GameLogic", qui connaît toutes les autres entités, (sans qu'aucune des autres entités ne le sache, ) afin que les autres entités n'aient pas à se connaître.

Par exemple, il me semble déraisonnable que votre entité RuleBook ait besoin de savoir quoi que ce soit sur l'entité GameState, car un livre de règles est quelque chose que nous consultons pour jouer, ce n'est pas quelque chose qui prend une part active au jeu. C'est donc cette nouvelle entité "Game" qui doit consulter à la fois le livre de règles et l'état du jeu afin de déterminer les mouvements disponibles, ce qui élimine les dépendances circulaires.

Maintenant, je pense que je peux deviner quel sera votre problème avec cette approche: coder l'entité "Game" de manière agnostique sera très difficile, donc vous allez probablement vous retrouver avec non seulement un mais deux les entités qui auront besoin d'implémentations sur mesure pour chaque type de jeu: l'entité "RuleBook" et l'entité "Game". Ce qui à son tour va à l'encontre du but d'avoir une entité "RuleBook" en premier lieu. Eh bien, tout ce que je peux dire à ce sujet, c'est que peut-être, juste peut-être, votre aspiration initiale à écrire un système qui peut jouer à de nombreux types de jeux peut avoir été noble, mais peut-être mal conçue. Si j'étais à votre place, je me serais concentré sur l'utilisation d'un mécanisme commun pour afficher l'état de tous les différents jeux, et un mécanisme commun pour recevoir les commentaires des utilisateurs pour tous ces jeux,

Mike Nakis
la source
1
Merci Mike. Vous avez raison sur les inconvénients de l'entité Game; avec l'ancien code d'applet, j'ai pu créer de nouveaux jeux avec à peine plus qu'une nouvelle sous-classe RuleBook et la conception graphique appropriée.
Damian Walker
10

La théorie des jeux traite les jeux comme une liste de mouvements précédents (types de valeur, y compris qui les a joués) et une fonction ValidMoves (previousMoves)

J'essayerais de suivre ce modèle pour la partie non UI du jeu et de traiter des choses comme la configuration du tableau comme des mouvements.

l'interface utilisateur peut alors être des trucs OO standard avec une référence à la logique


Mise à jour pour condenser les commentaires

Considérez les échecs. Les parties d'échecs sont généralement enregistrées sous forme de listes de coups. http://en.wikipedia.org/wiki/Portable_Game_Notation

la liste des coups définit bien mieux l'état complet du jeu qu'une image du plateau.

Disons par exemple que nous commençons à créer des objets pour Board, Piece, Move etc. et des méthodes comme Piece.GetValidMoves ()

nous voyons d'abord que nous devons avoir une pièce de référence sur la planche, mais ensuite nous considérons le roque. ce que vous ne pouvez faire que si vous n'avez pas encore déplacé votre roi ou votre tour. Nous avons donc besoin d'un drapeau MovedAlready sur le roi et les tours. De même, les pions peuvent déplacer 2 cases lors de leur premier mouvement.

Ensuite, nous voyons qu'en roquant le mouvement valide du roi dépend de l'existence et de l'état de la tour, donc le plateau doit avoir des pièces dessus et référencer ces pièces. nous abordons votre problème de référence circulaire.

Cependant, si nous définissons Move comme une structure immuable et un état de jeu comme la liste des mouvements précédents, nous constatons que ces problèmes disparaissent. Pour voir si le roque est valide, nous pouvons vérifier la liste des mouvements de l'existence des mouvements du château et du roi. Pour voir si le pion peut prendre en-passe, nous pouvons vérifier si l'autre pion a fait un double mouvement avant. Aucune référence n'est nécessaire sauf Règles -> Déplacer

Les échecs ont maintenant un tableau statique et les pièces sont toujours configurées de la même manière. Mais disons que nous avons une variante où nous autorisons une configuration alternative. peut-être en omettant certaines pièces comme handicap.

Si nous ajoutons les mouvements de configuration en tant que mouvements, «de la case au carré X» et adaptons l'objet Rules pour comprendre ce mouvement, alors nous pouvons toujours représenter le jeu comme une séquence de mouvements.

De même, si dans votre jeu, le plateau lui-même n'est pas statique, disons que nous pouvons ajouter des cases aux échecs ou supprimer des cases du plateau afin qu'elles ne puissent pas être déplacées. Ces modifications peuvent également être représentées sous la forme de mouvements sans modifier la structure globale de votre moteur de règles ni avoir à référencer un objet BoardSetup de type similaire.

Ewan
la source
Cela aura tendance à compliquer la mise en œuvre de ValidMoves, ce qui ralentira votre logique.
Taemyr
pas vraiment, je suppose que la configuration de la carte est variable, vous devez donc la définir d'une manière ou d'une autre. Si vous convertissez les mouvements de configuration en une autre structure ou un autre objet pour faciliter le calcul, vous pouvez mettre en cache le résultat si nécessaire. Certains jeux ont des planches qui changent avec le jeu et certains mouvements valides peuvent dépendre des mouvements précédents plutôt que de la position actuelle (par exemple, roquer aux échecs)
Ewan
1
Ajouter des drapeaux et des trucs est la complexité que vous évitez en ayant simplement l'historique des mouvements. ce n'est pas cher de boucler disons 100 coups d'échecs pour obtenir la configuration actuelle du plateau et vous pouvez mettre en cache le résultat entre les coups
Ewan
1
vous évitez également de modifier votre modèle d'objet pour refléter les règles. c'est-à-dire pour les échecs, si vous effectuez validMoves -> Piece + Board, vous échouez au roque, en-passent, premier mouvement pour les pions et la promotion de la pièce et devez ajouter des informations supplémentaires aux objets ou référencer un troisième objet. Vous perdez également l'idée de qui est parti et des concepts tels que le chèque découvert
Ewan
1
@Gabe Le boardLayoutest une fonction de tous priorMoves(c'est-à-dire que si nous le conservions comme état, rien ne serait apporté autre que chacun thisMove). Par conséquent, la suggestion d'Ewan est essentiellement «couper l'homme du milieu» - les mouvements valides sont une fonction directe de tous les précédents, au lieu de validMoves( boardLayout( priorMoves ) ).
OJFord
8

La manière standard de supprimer une référence circulaire entre deux classes dans la programmation orientée objet est d'introduire une interface qui peut ensuite être implémentée par l'une d'entre elles. Donc, dans votre cas, vous pourriez avoir fait RuleBookréférence à Statece qui fait alors référence à un InitialPositionProvider(qui serait une interface implémentée par RuleBook). Cela facilite également les tests, car vous pouvez ensuite créer un Statequi utilise une position initiale différente (probablement plus simple) à des fins de test.

Jules
la source
6

Je crois que les références circulaires et l'objet divin dans votre cas pourraient être facilement supprimés en séparant le contrôle du flux de jeu des modèles d'état et de règles du jeu. En faisant cela, vous gagneriez probablement beaucoup de flexibilité et vous débarrasser d'une complexité inutile.

Je pense que vous devriez avoir un contrôleur ("un maître de jeu" si vous le souhaitez) qui contrôle le déroulement du jeu et gère les changements d'état réels au lieu de confier cette responsabilité au livre de règles ou au jeu.

Un objet d'état de jeu n'a pas besoin de se changer ni d'être au courant des règles. La classe a juste besoin de fournir un modèle d'objets facilement manipulables (créés, inspectés, modifiés, persistants, journalisés, copiés, mis en cache, etc.) et efficaces pour le reste de l'application.

Le livre de règles ne devrait pas avoir besoin de connaître ou de jouer avec un jeu en cours. Il ne devrait avoir besoin que d'une vue d'un état de jeu pour pouvoir dire quels mouvements sont légaux et il n'a qu'à répondre avec un état de jeu résultant lorsqu'on lui demande ce qui se passe lorsqu'un mouvement est appliqué à un état de jeu. Il pourrait également fournir un état de début de jeu lorsqu'on lui a demandé une disposition initiale.

Le contrôleur doit être au courant des états du jeu et du livre de règles et peut-être de certains autres objets du modèle de jeu, mais il ne devrait pas avoir à s'embêter avec les détails.

VIENS DE
la source
4
EXACTEMENT ma pensée. L'OP mélange trop de données et de procédures dans les mêmes classes. Il vaut mieux les répartir davantage. C'est un bon discours sur le sujet. Au fait, quand je lis "voir un état de jeu", je pense "argument à la fonction". +100 si je pouvais.
jpmc26
5

Je pense que le problème ici est que vous n'avez pas donné une description claire de quelles tâches doivent être gérées par quelles classes. Je décrirai ce que je pense être une bonne description de ce que chaque classe devrait faire, puis je donnerai un exemple de code générique qui illustre les idées. Nous verrons que le code est moins couplé, et donc il n'a pas vraiment de références circulaires.

Commençons par décrire ce que fait chaque classe.

La GameStateclasse ne doit contenir que des informations sur l'état actuel du jeu. Il ne doit contenir aucune information sur ce que les états passés du jeu ou quels mouvements futurs sont possibles. Il ne doit contenir que des informations sur les pièces sur les cases des échecs ou sur le nombre et le type de pions sur les points du backgammon. leGameState devra contenir des informations supplémentaires, comme des informations sur le roque aux échecs ou sur le cube doublant au backgammon.

La Moveclasse est un peu délicate. Je dirais que je peux spécifier un coup à jouer en spécifiant celui GameStatequi résulte de la lecture du coup. Vous pouvez donc imaginer qu'un mouvement peut simplement être implémenté en tant que GameState. Cependant, dans go (par exemple), vous pourriez imaginer qu'il est beaucoup plus facile de spécifier un mouvement en spécifiant un seul point sur la carte. Nous voulons que notre Moveclasse soit suffisamment flexible pour gérer l'un ou l'autre de ces cas. Par conséquent, la Moveclasse va en fait être une interface avec une méthode qui prend un pré-mouvement GameStateet retourne un nouveau post-mouvementGameState .

Maintenant, la RuleBookclasse est responsable de tout savoir sur les règles. Cela peut être décomposé en trois choses. Il doit savoir quelle est l'initiale GameState, il doit savoir quels mouvements sont légaux, et il doit pouvoir savoir si l'un des joueurs a gagné.

Vous pouvez également créer un GameHistorycours pour garder une trace de tous les mouvements qui ont été effectués et de tous ceux GameStatesqui se sont produits. Une nouvelle classe est nécessaire parce que nous avons décidé qu'un seul GameStatene devrait pas être responsable de connaître tous les GameStates qui l'ont précédé.

Ceci conclut les classes / interfaces dont je parlerai. Vous avez également unBoard classe. Mais je pense que les planches des différents jeux sont suffisamment différentes pour qu'il soit difficile de voir ce qui pourrait être fait génériquement avec les planches. Je vais maintenant donner des interfaces génériques et implémenter des classes génériques.

Le premier est GameState. Puisque cette classe dépend complètement du jeu particulier, il n'y a pas d' Gamestateinterface ou de classe générique .

Le suivant est Move. Comme je l'ai dit, cela peut être représenté par une interface qui a une seule méthode qui prend un état pré-mouvement et produit un état post-mouvement. Voici le code de cette interface:

package boardgame;

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

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Notez qu'il existe un paramètre de type. En effet, par exemple, ChessMoveil faudra connaître les détails du pré-déménagement ChessGameState. Ainsi, par exemple, la déclaration de classe de ChessMoveserait

class ChessMove extends Move<ChessGameState>,

où vous auriez déjà défini une ChessGameStateclasse.

Ensuite, je vais discuter de la RuleBookclasse générique . Voici le 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);

}

Encore une fois, il existe un paramètre de type pour la GameStateclasse. Puisque le RuleBookest supposé savoir quel est l'état initial, nous avons mis une méthode pour donner l'état initial. Puisque le RuleBookest censé savoir quels mouvements sont légaux, nous avons des méthodes pour tester si un mouvement est légal dans un état donné et pour donner une liste des mouvements légaux pour un état donné. Enfin, il existe une méthode pour évaluer le GameState. Remarquez que le RuleBookdevrait seulement être responsable de décrire si l'un ou l'autre des joueurs a déjà gagné, mais pas qui est mieux placé au milieu d'une partie. Décider qui est dans une meilleure position est une chose compliquée qui devrait être déplacée dans sa propre classe. Par conséquent, la StateEvaluationclasse n'est en fait qu'une simple énumération donnée comme suit:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Enfin, décrivons la GameHistoryclasse. Cette classe est chargée de se souvenir de toutes les positions qui ont été atteintes dans le jeu ainsi que des mouvements qui ont été joués. La principale chose qu'il devrait pouvoir faire est d'enregistrer un Movetel que joué. Vous pouvez également ajouter des fonctionnalités pour annuler les Moves. J'ai une implémentation ci-dessous.

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();
    }

}

Enfin, nous pourrions imaginer faire un Gamecours pour tout lier ensemble. Cette Gameclasse est censée exposer des méthodes qui permettent aux gens de voir quel est le courant GameState, de voir qui, si quelqu'un en a un, de voir quels coups peuvent être joués et de jouer un coup. J'ai une implémentation ci-dessous

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());
    }

}

Notez dans cette classe que le RuleBookn'est pas responsable de savoir quel est le courant GameState. C'est ça le GameHistoryboulot. Donc, le Gamedemande l' GameHistoryétat actuel et donne ces informations au RuleBookmoment où il Gamefaut dire quels sont les mouvements légaux ou si quelqu'un a gagné.

Quoi qu'il en soit, le point de cette réponse est qu'une fois que vous avez déterminé de manière raisonnable les responsabilités de chaque classe et que vous concentrez chaque classe sur un petit nombre de responsabilités, et que vous attribuez chaque responsabilité à une classe unique, puis les classes ont tendance à être découplés, et tout devient facile à coder. J'espère que cela ressort des exemples de code que j'ai donnés.

Brian Moths
la source
3

D'après mon expérience, les références circulaires indiquent généralement que votre conception n'est pas bien pensée.

Dans votre conception, je ne comprends pas pourquoi RuleBook doit "connaître" l'État. Il peut recevoir un Etat en tant que paramètre à une méthode, bien sûr, mais pourquoi devrait - il besoin de savoir (c. -à- prise comme une variable d'instance) une référence à un État? Cela n'a aucun sens pour moi. Un RuleBook n'a pas besoin de "connaître" l'état d'un jeu particulier pour faire son travail; les règles du jeu ne changent pas en fonction de l'état actuel du jeu. Donc, soit vous l'avez mal conçu, soit vous l'avez conçu correctement mais vous l'expliquez incorrectement.

Mehrdad
la source
+1. Vous achetez un jeu de plateau physique, vous obtenez un livre de règles capable de décrire les règles sans état.
unperson325680
1

La dépendance circulaire n'est pas nécessairement un problème technique, mais elle doit être considérée comme une odeur de code, qui est généralement une violation du principe de responsabilité unique .

Votre dépendance circulaire vient du fait que vous essayez d'en faire trop avec votre Stateobjet.

Tout objet avec état ne doit fournir que des méthodes directement liées à la gestion de cet état local. S'il nécessite autre chose que la logique la plus élémentaire, il doit probablement être divisé en un modèle plus large. Certaines personnes ont des opinions différentes à ce sujet, mais en règle générale, si vous faites plus que des getters et des setters sur les données, vous en faites trop.

Dans ce cas, vous feriez mieux d'avoir un StateFactory , qui pourrait connaître un Rulebook. Vous auriez probablement une autre classe de contrôleur qui utilise votre StateFactorypour créer un nouveau jeu. Statene devrait certainement pas savoir Rulebook. Rulebookpourrait connaître un en Statefonction de la mise en œuvre de vos règles.

00500005
la source
0

Existe-t-il un besoin pour un objet de livre de règles d'être lié à un état de jeu particulier, ou serait-il plus logique d'avoir un objet de livre de règles avec une méthode qui, étant donné un état de jeu, rendra compte des mouvements disponibles à partir de cet état (et, après avoir signalé cela, ne me souviens pas de l'état en question)? À moins qu'il y ait quelque chose à gagner à ce que l'objet interrogé sur les mouvements disponibles conserve une mémoire de l'état du jeu, il n'est pas nécessaire qu'il conserve une référence.

Il est possible dans certains cas que l'état d'évaluation de l'objet d'évaluation des règles présente des avantages. Si vous pensez qu'une telle situation peut se produire, je suggérerais d'ajouter une classe "arbitre" et de faire en sorte que le livre de règles fournisse une méthode "createReferee". Contrairement au règlement, qui ne se soucie pas de savoir s'il est question d'un match ou de cinquante, un arbitre objet s'attendrait à arbitrer un match. Il ne devrait pas encapsuler tous les états liés au jeu dont il arbitre, mais pourrait mettre en cache toutes les informations sur le jeu qu'il jugerait utiles. Si un jeu prend en charge la fonctionnalité "annuler", il peut être utile que l'arbitre inclue un moyen de produire un objet "instantané" qui pourrait être stocké avec des états de jeu antérieurs; cet objet devrait,

Si un couplage peut être nécessaire entre les aspects du traitement des règles et du traitement de l'état du jeu du code, l'utilisation d'un objet arbitre permettra de garder un tel couplage en dehors du livre de règles principal et des classes d'état du jeu. Cela peut également permettre à de nouvelles règles de prendre en compte des aspects de l'état du jeu que la classe d'état du jeu ne considérerait pas comme pertinents (par exemple, si une règle a été ajoutée qui dit que "l'objet X ne peut pas faire Y s'il a déjà été à l'emplacement Z"). ", l'arbitre pourrait être changé pour garder une trace des objets qui se sont rendus à l'emplacement Z sans avoir à changer la classe d'état du jeu).

supercat
la source
-2

La bonne façon de gérer cela est d'utiliser des interfaces. Au lieu d'avoir deux classes qui se connaissent, demandez à chaque classe d'implémenter une interface et de la référencer dans l'autre classe. Disons que vous avez des classes A et B qui doivent se référencer mutuellement. Ayez l'interface d'implémentation A de classe A et l'interface B d'implémentation de classe B, vous pouvez référencer l'interface B de la classe A et l'interface A de la classe B. La classe A peut être dans son propre projet, tout comme la classe B. Les interfaces sont dans un projet séparé auxquels se réfèrent les deux autres projets.

Peter
la source
2
cela semble simplement répéter les points avancés et expliqués dans une réponse antérieure publiée quelques heures avant celle-ci
moucher