web-dev-qa-db-fra.com

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?

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 être connus sur l'autre.

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 en 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 se déplace 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 a besoin de tout savoir. Mais cela oblige à déplacer de nombreuses méthodes dans la classe RuleBook (comme faire 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!

24
Damian Walker

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érence. Les références circulaires ne causent aucun type de problème en Java. Temps passé à éliminer les références circulaires parfaitement naturelles en Java est du temps perdu.

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 par bourrant toutes les classes entrelacées dans un seul fichier source, [...]

Pas nécessaire. Si vous compilez 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. La compilation de tous les fichiers Java source qui appartiennent au même package à la fois n'est pas un hack intelligent, c'est précisément la façon Java is supposé fonctionner.

47
Atsby

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ématique, comme vous semblez les considérer comme être: ils sont parfaitement légaux 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 Java ne sont même pas conscients du fait que vous pouvez déclarer une classe dans une méthode, sans parler de 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.

Pour 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 de simples étapes prédéterminées "sans cervelle" 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 vous devez 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 ce dont vous avez besoin est 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 à jouer. C'est donc cette nouvelle entité "Jeu" 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 pas 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, et j'aurais alors autorisé la mise en œuvre des différents jeux de varier énormément, ayant très peu de code en commun, afin de garantir une flexibilité maximale dans la mise en œuvre de chaque jeu.

22
Mike Nakis

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 standard OO truc 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 comme des mouvements sans modifier la structure globale de votre moteur de règles ou sans avoir à référencer un BoardSetup objet similaire

10
Ewan

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'elles. Donc dans votre cas, vous pourriez avoir RuleBook faisant référence à State 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 State qui utilise une position initiale différente (vraisemblablement plus simple) à des fins de test.

8
Jules

Je pense 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.

6
COME FROM

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 classe GameState 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. Le GameState devra contenir des informations supplémentaires, comme des informations sur le roque des échecs ou sur le cube doublant dans le backgammon.

La classe Move est un peu délicate. Je dirais que je peux spécifier un coup à jouer en spécifiant le GameState qui 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 classe Move soit suffisamment flexible pour gérer ces deux cas. Par conséquent, la classe Move va réellement être une interface avec une méthode qui prend un pré-déplacement GameState et retourne un nouveau post-déplacement GameState.

Maintenant, la classe RuleBook est responsable de tout savoir sur les règles. Cela peut être décomposé en trois choses. Il doit savoir quel est le GameState initial, 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 une classe GameHistory pour garder une trace de tous les mouvements effectués et de tous les GameStates qui se sont produits. Une nouvelle classe est nécessaire car nous avons décidé qu'un seul GameState ne devrait pas être responsable de la connaissance de tous les GameState qui l'ont précédé.

Ceci conclut les classes/interfaces dont je parlerai. Vous avez également une classe Board. 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'interface ni de classe générique Gamestate.

Vient ensuite 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é-déplacement et produit un état post-déplacement. 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, un ChessMove devra connaître les détails du pré-déplacement ChessGameState. Ainsi, par exemple, la déclaration de classe de ChessMove serait

class ChessMove extends Move<ChessGameState>,

où vous auriez déjà défini une classe ChessGameState.

Ensuite, je vais discuter de la classe générique RuleBook. 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 classe GameState. Puisque le RuleBook est censé savoir quel est l'état initial, nous avons mis une méthode pour donner l'état initial. Puisque le RuleBook est 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. Notez que le RuleBook ne doit être responsable que de décrire si l'un ou l'autre des joueurs a déjà gagné, mais pas celui qui est le 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 classe StateEvaluation 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 classe GameHistory. Cette classe est chargée de se souvenir de toutes les positions atteintes dans le jeu ainsi que des coups joués. La principale chose qu'il devrait pouvoir faire est d'enregistrer un Move tel qu'il est joué. Vous pouvez également ajouter des fonctionnalités pour annuler 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 créer une classe Game pour tout lier ensemble. Cette classe Game est censée exposer des méthodes qui permettent aux gens de voir quel est le GameState actuel, de voir qui, si quelqu'un en a un, de voir quels mouvements peuvent être joués et de jouer un bouge toi. 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 RuleBook n'est pas responsable de savoir quel est le GameState actuel. C'est le travail de GameHistory. Ainsi, le Game demande au GameHistory quel est l'état actuel et donne ces informations au RuleBook lorsque le Game a besoin de 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.

5
Brian Moths

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 pourrait recevoir un État sous la forme d'un paramètre à une méthode, bien sûr, mais pourquoi devrait-il avoir savoir (c'est-à-dire contenir comme 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.

3
user541686

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 objet State.

Tout objet avec état ne doit fournir que des méthodes directement liées à la gestion de cet état local. Si elle nécessite autre chose que la logique la plus élémentaire, elle devrait probablement être divisée 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 autre chose 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 StateFactory pour créer un nouveau jeu. State ne devrait certainement pas connaître Rulebook. Rulebook peut connaître un State selon l'implémentation de vos règles.

1
00500005

Est-il nécessaire qu'un objet de livre de règles soit 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, signalera les 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, étant donné une copie de l'état du jeu lors de sa création, être capable à son tour de produire un nouvel arbitre dont l'état devrait correspondre à l'état de l'arbitre d'origine lors de la création de l'instantané.

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 conserver un tel couplage out du livre de règles principal et de l'état du jeu Des classes. 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).

0
supercat