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!
la source
RuleBook
prend par exemple leState
comme argument et retourne le valideMoveList
, c'est-à-dire "voici où nous en sommes maintenant, que peut-on faire ensuite?"Réponses:
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.
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.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.
la source
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.)
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.
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,
la source
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.
la source
boardLayout
est une fonction de touspriorMoves
(c'est-à-dire que si nous le conservions comme état, rien ne serait apporté autre que chacunthisMove
). 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 devalidMoves( boardLayout( priorMoves ) )
.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
RuleBook
référence àState
ce qui fait alors référence à unInitialPositionProvider
(qui serait une interface implémentée parRuleBook
). Cela facilite également les tests, car vous pouvez ensuite créer unState
qui utilise une position initiale différente (probablement plus simple) à des fins de test.la source
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.
la source
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
GameState
classe 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
Move
classe est un peu délicate. Je dirais que je peux spécifier un coup à jouer en spécifiant celuiGameState
qui résulte de la lecture du coup. Vous pouvez donc imaginer qu'un mouvement peut simplement être implémenté en tant queGameState
. 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 notreMove
classe soit suffisamment flexible pour gérer l'un ou l'autre de ces cas. Par conséquent, laMove
classe va en fait être une interface avec une méthode qui prend un pré-mouvementGameState
et retourne un nouveau post-mouvementGameState
.Maintenant, la
RuleBook
classe est responsable de tout savoir sur les règles. Cela peut être décomposé en trois choses. Il doit savoir quelle est l'initialeGameState
, 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
GameHistory
cours pour garder une trace de tous les mouvements qui ont été effectués et de tous ceuxGameStates
qui se sont produits. Une nouvelle classe est nécessaire parce que nous avons décidé qu'un seulGameState
ne devrait pas être responsable de connaître tous lesGameState
s qui l'ont précédé.Ceci conclut les classes / interfaces dont je parlerai. Vous avez également un
Board
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'Gamestate
interface 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:Notez qu'il existe un paramètre de type. En effet, par exemple,
ChessMove
il faudra connaître les détails du pré-déménagementChessGameState
. Ainsi, par exemple, la déclaration de classe deChessMove
seraitclass ChessMove extends Move<ChessGameState>
,où vous auriez déjà défini une
ChessGameState
classe.Ensuite, je vais discuter de la
RuleBook
classe générique . Voici le code:Encore une fois, il existe un paramètre de type pour la
GameState
classe. Puisque leRuleBook
est supposé savoir quel est l'état initial, nous avons mis une méthode pour donner l'état initial. Puisque leRuleBook
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 leGameState
. Remarquez que leRuleBook
devrait 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, laStateEvaluation
classe n'est en fait qu'une simple énumération donnée comme suit:Enfin, décrivons la
GameHistory
classe. 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 unMove
tel que joué. Vous pouvez également ajouter des fonctionnalités pour annuler lesMove
s. J'ai une implémentation ci-dessous.Enfin, nous pourrions imaginer faire un
Game
cours pour tout lier ensemble. CetteGame
classe est censée exposer des méthodes qui permettent aux gens de voir quel est le courantGameState
, 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-dessousNotez dans cette classe que le
RuleBook
n'est pas responsable de savoir quel est le courantGameState
. C'est ça leGameHistory
boulot. Donc, leGame
demande l'GameHistory
état actuel et donne ces informations auRuleBook
moment où ilGame
faut 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.
la source
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.
la source
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
State
objet.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 unRulebook
. Vous auriez probablement une autre classe de contrôleur qui utilise votreStateFactory
pour créer un nouveau jeu.State
ne devrait certainement pas savoirRulebook
.Rulebook
pourrait connaître un enState
fonction de la mise en œuvre de vos règles.la source
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).
la source
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.
la source