He estado luchando con un problema en un proyecto Java sobre referencias circulares. Estoy tratando de modelar una situación del mundo real en la que parece que los objetos en cuestión son interdependientes y necesitan conocerse unos a otros.
El proyecto es un modelo genérico de jugar un juego de mesa. Las clases básicas no son específicas, pero se extienden para tratar con detalles específicos de ajedrez, backgammon y otros juegos. Codifiqué esto como un applet hace 11 años con media docena de juegos diferentes, pero el problema es que está lleno de referencias circulares. Lo implementé en ese entonces rellenando todas las clases entrelazadas en un solo archivo fuente, pero tengo la idea de que esa es una forma incorrecta en Java. Ahora quiero implementar algo similar a una aplicación de Android, y quiero hacer las cosas correctamente.
Las clases son:
RuleBook: un objeto que puede ser interrogado por cosas como el diseño inicial del tablero, otra información inicial del estado del juego, como quién se mueve primero, los movimientos disponibles, qué sucede con el estado del juego después de un movimiento propuesto y una evaluación de una posición actual o propuesta de la junta.
Tablero: una representación simple de un tablero de juego, que puede ser instruido para reflejar un movimiento.
MoveList: una lista de movimientos. Esto es de doble propósito: una selección de movimientos disponibles en un punto dado, o una lista de movimientos que se han realizado en el juego. Podría dividirse en dos clases casi idénticas, pero eso no es relevante para la pregunta que estoy haciendo y puede complicarlo aún más.
Movimiento: un solo movimiento. Incluye todo sobre el movimiento como una lista de átomos: recoge una pieza de aquí, ponla allí, retira una pieza capturada de allí.
Estado: la información de estado completa de un juego en progreso. No solo la posición de la Junta, sino una MoveList y otra información del estado, como quién se va a mover ahora. En el ajedrez se registraría si el rey y las torres de cada jugador se han movido.
Abundan las referencias circulares, por ejemplo: el RuleBook necesita saber sobre el Estado del juego para determinar qué movimientos están disponibles en un momento dado, pero el Estado del juego debe consultar el RuleBook para el diseño inicial y los efectos secundarios que acompañan a un movimiento una vez está hecho (por ejemplo, quién se mueve a continuación).
Intenté organizar el nuevo conjunto de clases jerárquicamente, con RuleBook en la parte superior, ya que necesita saber sobre todo. Pero esto resulta en tener que mover muchos métodos a la clase RuleBook (como hacer un movimiento), lo que lo hace monolítico y no particularmente representativo de lo que debería ser un RuleBook.
Entonces, ¿cuál es la forma correcta de organizar esto? ¿Debo convertir RuleBook en BigClassThatDoesAlmostEverythingInTheGame para evitar referencias circulares, abandonando el intento de modelar el juego del mundo real con precisión? ¿O debería seguir con las clases interdependientes y convencer al compilador para que las compile de alguna manera, conservando mi modelo del mundo real? ¿O hay alguna estructura válida obvia que me falta?
¡Gracias por cualquier ayuda que usted puede dar!
fuente
RuleBook
tomamos, por ejemplo, elState
como argumento, y devolvimos el válidoMoveList
, es decir, "aquí es donde estamos ahora, qué se puede hacer a continuación?"Respuestas:
El recolector de basura de Java no se basa en técnicas de conteo de referencias. Las referencias circulares no causan ningún tipo de problema en Java. El tiempo dedicado a eliminar referencias circulares perfectamente naturales en Java es una pérdida de tiempo.
No es necesario. Si solo compila todos los archivos de origen a la vez (por ejemplo,
javac *.java
), el compilador resolverá todas las referencias directas sin problemas.Sí. Se espera que las clases de aplicación sean interdependientes. Compilar todos los archivos fuente de Java que pertenecen al mismo paquete a la vez no es un truco inteligente, es precisamente la forma en que se supone que funciona Java .
fuente
Por supuesto, las dependencias circulares son una práctica cuestionable desde el punto de vista del diseño, pero no están prohibidas y, desde un punto de vista puramente técnico, ni siquiera son necesariamente problemáticas , como parece considerarlas: son perfectamente legales en En la mayoría de los escenarios, son inevitables en algunas situaciones, y en algunas raras ocasiones incluso pueden considerarse como algo útil.
En realidad, hay muy pocos escenarios en los que el compilador de Java negará una dependencia circular. (Nota: puede haber más, solo puedo pensar en lo siguiente en este momento).
En herencia: no puede tener una clase A extender la clase B que a su vez extiende la clase A, y es perfectamente razonable que no pueda tener esto, ya que la alternativa no tendría ningún sentido desde un punto de vista lógico.
Entre las clases de método local: las clases declaradas dentro de un método pueden no hacer referencia circular entre sí. Probablemente esto no sea más que una limitación del compilador de Java, posiblemente porque la capacidad de hacer tal cosa no es lo suficientemente útil como para justificar la complejidad adicional que tendría que ir al compilador para admitirlo. (La mayoría de los programadores de Java ni siquiera son conscientes del hecho de que puede declarar una clase dentro de un método, y mucho menos declarar varias clases, y luego hacer que estas clases hagan referencia circular entre sí).
Por lo tanto, es importante darse cuenta y evitar que la búsqueda para minimizar las dependencias circulares sea una búsqueda de la pureza del diseño, no una búsqueda de la corrección técnica.
Hasta donde sé, no existe un enfoque reduccionista para eliminar las dependencias circulares, lo que significa que no hay una receta que consista en nada más que simples pasos predeterminados "obvios" para tomar un sistema con referencias circulares, aplicarlas una tras otra y finalizar con un sistema libre de referencias circulares. Debe poner su mente a trabajar y debe realizar pasos de refactorización que dependen de la naturaleza de su diseño.
En la situación particular que tiene a mano, me parece que lo que necesita es una nueva entidad, quizás llamada "Juego" o "GameLogic", que conoce todas las demás entidades (sin que ninguna de las otras entidades lo sepa, ) para que las otras entidades no tengan que conocerse.
Por ejemplo, me parece irrazonable que su entidad RuleBook necesite saber algo sobre la entidad GameState, porque un libro de reglas es algo que consultamos para jugar, no es algo que participe activamente en el juego. Entonces, es esta nueva entidad de "Juego" la que necesita consultar tanto el libro de reglas como el estado del juego para determinar qué movimientos están disponibles, y esto elimina las dependencias circulares.
Ahora, creo que puedo adivinar cuál será su problema con este enfoque: codificar la entidad "Juego" de una manera independiente del juego será muy difícil, por lo que lo más probable es que termine con no solo uno sino dos entidades que necesitarán implementaciones personalizadas para cada tipo diferente de juego: el "RuleBook" y la entidad "Game". Lo que a su vez anula el propósito de tener una entidad "RuleBook" en primer lugar. Bueno, todo lo que puedo decir sobre esto es que tal vez, solo tal vez, tu aspiración inicial de escribir un sistema que pueda jugar muchos tipos diferentes de juegos puede haber sido noble, pero tal vez mal concebida. Si estuviera en tu lugar, me habría centrado en usar un mecanismo común para mostrar el estado de todos los diferentes juegos, y un mecanismo común para recibir la entrada del usuario para todos estos juegos,
fuente
La teoría de juegos trata los juegos como una lista de movimientos previos (tipos de valor que incluyen quién los jugó) y una función ValidMoves (previousMoves)
Intentaría seguir este patrón para la parte del juego que no sea UI y trataría cosas como la configuración del tablero como movimientos.
la interfaz de usuario puede ser material estándar de OO con una forma de referencia a la lógica
Actualización para condensar comentarios
Considera el ajedrez. Los juegos de ajedrez se registran comúnmente como listas de movimientos. http://en.wikipedia.org/wiki/Portable_Game_Notation
La lista de movimientos define el estado completo del juego mucho mejor que una imagen del tablero.
Digamos, por ejemplo, que comenzamos a hacer objetos para Tablero, Pieza, Mover, etc. y Métodos como Piece.GetValidMoves ()
Primero vemos que tenemos que tener una pieza que haga referencia al tablero, pero luego consideramos enrocar. lo que solo puedes hacer si aún no has movido a tu rey o torre. Entonces necesitamos una bandera MovedAlady ya en el rey y las torres. Del mismo modo, los peones pueden mover 2 casillas en su primer movimiento.
Luego vemos que en el enroque, el movimiento válido del rey depende de la existencia y el estado de la torre, por lo que el tablero debe tener piezas y hacer referencia a esas piezas. Estamos entrando en su problema de referencia circular.
Sin embargo, si definimos Move como una estructura inmutable y el estado del juego como la lista de movimientos anteriores, encontramos que estos problemas desaparecen. Para ver si el enroque es válido, podemos verificar la lista de movimientos de la existencia de movimientos de castillo y rey. Para ver si el peón puede pasar, podemos verificar si el otro peón hizo un doble movimiento en el movimiento anterior. No se necesitan referencias excepto Reglas -> Mover
Ahora el ajedrez tiene un tablero estático, y las piezas siempre se configuran de la misma manera. Pero digamos que tenemos una variante donde permitimos una configuración alternativa. quizás omitiendo algunas piezas como una desventaja.
Si agregamos los movimientos de configuración como movimientos, 'de la casilla al cuadrado X' y adaptamos el objeto Reglas para comprender ese movimiento, aún podemos representar el juego como una secuencia de movimientos.
Del mismo modo, si en su juego el tablero en sí no es estático, digamos que podemos agregar cuadrados al ajedrez o eliminar cuadrados del tablero para que no se puedan mover. Estos cambios también se pueden representar como Movimientos sin cambiar la estructura general de su motor de Reglas o tener que hacer referencia a un objeto BoardSetup de similar
fuente
boardLayout
es una función de todospriorMoves
(es decir, si lo mantuviéramos como estado, no se aportaría nada más que cada unothisMove
). Por lo tanto, la sugerencia de Ewan es esencialmente "cortar al intermediario": los movimientos válidos son una función directa de todos los anteriores, en lugar devalidMoves( boardLayout( priorMoves ) )
.La forma estándar de eliminar una referencia circular entre dos clases en la programación orientada a objetos es introducir una interfaz que luego pueda implementar una de ellas. Entonces, en su caso, podría haber hecho
RuleBook
referencia a loState
que luego se refiere a unInitialPositionProvider
(que sería una interfaz implementada porRuleBook
). Esto también facilita las pruebas, ya que puede crear unaState
que use una posición inicial diferente (presumiblemente más simple) para fines de prueba.fuente
Creo que las referencias circulares y el objeto dios en su caso podrían eliminarse fácilmente separando el control del flujo del juego del estado y los modelos de reglas del juego. Al hacerlo, probablemente obtendría mucha flexibilidad y se libraría de una complejidad innecesaria.
Creo que debería tener un controlador ("un maestro del juego" si lo desea) que controla el flujo del juego y maneja los cambios de estado reales en lugar de otorgarle al libro de reglas o al juego esta responsabilidad.
Un objeto de estado del juego no necesita cambiarse a sí mismo ni conocer las reglas. La clase solo necesita proporcionar un modelo de objetos de estado del juego fáciles de manejar (creados, inspeccionados, alterados, persistentes, registrados, copiados, almacenados en caché, etc.) y eficientes para el resto de la aplicación.
El libro de reglas no debería necesitar conocer ni jugar con ningún juego en curso. Solo debería necesitar una vista de un estado de juego para poder saber qué movimientos son legales y solo necesita responder con un estado de juego resultante cuando se le pregunta qué sucede cuando un movimiento se aplica a un estado de juego. También podría proporcionar un estado inicial del juego cuando se le solicita un diseño inicial.
El controlador debe conocer los estados del juego y el libro de reglas y quizás algunos otros objetos del modelo del juego, pero no debería tener que meterse con los detalles.
fuente
Creo que el problema aquí es que no ha dado una descripción clara de qué tareas deben ser manejadas por qué clases. Describiré lo que creo que es una buena descripción de lo que debe hacer cada clase, luego daré un ejemplo de código genérico que ilustra las ideas. Veremos que el código está menos acoplado, por lo que en realidad no tiene referencias circulares.
Comencemos describiendo lo que hace cada clase.
La
GameState
clase solo debe contener información sobre el estado actual del juego. No debe contener ninguna información sobre cuáles son los estados pasados del juego o qué movimientos futuros son posibles. Solo debe contener información sobre qué piezas están en qué cuadros del ajedrez, o cuántos y qué tipo de fichas están en qué puntos del backgammon. ElGameState
tendrá que contener alguna información adicional, como información sobre el enroque en el ajedrez o alrededor de la duplicación del cubo en el backgammon.La
Move
clase es un poco complicada. Yo diría que puedo especificar un movimiento para jugar especificando elGameState
resultado de jugar el movimiento. Así que podrías imaginar que un movimiento solo se puede implementar como aGameState
. Sin embargo, en go (por ejemplo) podría imaginar que es mucho más fácil especificar un movimiento especificando un solo punto en el tablero. Queremos que nuestraMove
clase sea lo suficientemente flexible como para manejar cualquiera de estos casos. Por lo tanto, laMove
clase realmente será una interfaz con un método que toma un movimiento previoGameState
y devuelve un nuevo movimiento posteriorGameState
.Ahora la
RuleBook
clase es responsable de saber todo sobre las reglas. Esto se puede dividir en tres cosas. Necesita saber cuál es la inicialGameState
, necesita saber qué movimientos son legales y necesita saber si uno de los jugadores ha ganado.También puede hacer una
GameHistory
clase para realizar un seguimiento de todos los movimientos que se han realizado y todo loGameStates
que ha sucedido. Es necesaria una nueva clase porque decidimos que una solaGameState
no debería ser responsable de conocer todos losGameState
mensajes anteriores.Esto concluye las clases / interfaces que discutiré. También tienes una
Board
clase. Pero creo que los tableros en diferentes juegos son lo suficientemente diferentes como para que sea difícil ver qué podría hacerse genéricamente con los tableros. Ahora voy a dar interfaces genéricas e implementar clases genéricas.En primer lugar es
GameState
. Dado que esta clase depende completamente del juego en particular, no existe unaGamestate
interfaz o clase genérica .El siguiente es
Move
. Como dije, esto se puede representar con una interfaz que tiene un único método que toma un estado previo al movimiento y produce un estado posterior al movimiento. Aquí está el código para esta interfaz:Tenga en cuenta que hay un parámetro de tipo. Esto se debe a que, por ejemplo,
ChessMove
será necesario conocer los detalles del pre-movimientoChessGameState
. Entonces, por ejemplo, la declaración de clase deChessMove
seríaclass ChessMove extends Move<ChessGameState>
,donde ya habrías definido una
ChessGameState
clase.A continuación hablaré sobre la
RuleBook
clase genérica . Aquí está el código:Nuevamente hay un parámetro de tipo para la
GameState
clase. ComoRuleBook
se supone que sabe cuál es el estado inicial, hemos puesto un método para dar el estado inicial. Dado queRuleBook
se supone que sabe qué movimientos son legales, tenemos métodos para probar si un movimiento es legal en un estado determinado y para dar una lista de movimientos legales para un estado determinado. Finalmente, hay un método para evaluar elGameState
. Tenga en cuenta que elRuleBook
solo debe ser responsable de describir si uno u otro jugador ya ha ganado, pero no quién está en una mejor posición en medio de un juego. Decidir quién está en una mejor posición es algo complicado que debería trasladarse a su propia clase. Por lo tanto, laStateEvaluation
clase es en realidad solo una enumeración simple dada de la siguiente manera:Por último, describamos la
GameHistory
clase. Esta clase es responsable de recordar todas las posiciones que se alcanzaron en el juego, así como los movimientos que se jugaron. Lo principal que debería poder hacer es grabar unaMove
reproducción. También puede agregar funcionalidad para deshacerMove
s. Tengo una implementación a continuación.Finalmente, podríamos imaginar hacer una
Game
clase para unir todo. SeGame
supone que esta clase expone métodos que permiten a las personas ver cuál es la corrienteGameState
, ver quién, si alguien tiene uno, ver qué movimientos se pueden jugar y jugar un movimiento. Tengo una implementación a continuaciónObserve en esta clase que
RuleBook
no es responsable de saber cuál es la corrienteGameState
. Ese es elGameHistory
trabajo de. Entonces,Game
preguntaGameHistory
cuál es el estado actual y le da esta información aRuleBook
cuándoGame
necesita decir cuáles son los movimientos legales o si alguien ha ganado.De todos modos, el punto de esta respuesta es que una vez que haya hecho una determinación razonable de lo que cada clase es responsable, y hace que cada clase se centre en un pequeño número de responsabilidades, y asigne cada responsabilidad a una clase única, entonces las clases tienden a estar desacoplados, y todo se vuelve fácil de codificar. Esperemos que sea evidente por los ejemplos de código que di.
fuente
En mi experiencia, las referencias circulares generalmente indican que su diseño no está bien pensado.
En su diseño, no entiendo por qué RuleBook necesita "saber" sobre el Estado. Puede recibir un Estado como parámetro de algún método, claro, pero ¿por qué debería necesitar saber (es decir, mantener como una variable de instancia) una referencia a un Estado? Eso no tiene sentido para mí. Un RuleBook no necesita "saber" sobre el estado de un juego en particular para hacer su trabajo; Las reglas del juego no cambian dependiendo del estado actual del juego. Entonces, o lo has diseñado incorrectamente, o lo has diseñado correctamente pero lo estás explicando incorrectamente.
fuente
La dependencia circular no es necesariamente un problema técnico, pero debe considerarse un olor a código, que generalmente es una violación del Principio de responsabilidad única .
Su dependencia circular proviene del hecho de que está tratando de hacer demasiado de su
State
objeto.Cualquier objeto con estado solo debe proporcionar métodos que se relacionen directamente con la administración de ese estado local. Si requiere algo más que la lógica más básica, entonces probablemente debería dividirse en un patrón más grande. Algunas personas tienen opiniones diferentes sobre esto, pero como regla general, si está haciendo algo más que captadores y establecedores de datos, está haciendo demasiado.
En este caso, sería mejor tener un
StateFactory
, que podría saber sobre unRulebook
. Probablemente tendrías otra clase de controlador que usa tuStateFactory
para crear un nuevo juego.State
definitivamente no debería saberloRulebook
.Rulebook
podría saber acerca de unaState
dependencia de la implementación de sus reglas.fuente
¿Hay alguna necesidad de que un objeto del libro de reglas esté vinculado a un estado particular del juego, o tendría más sentido tener un objeto del libro de reglas con un método que, dado un estado del juego, informará qué movimientos están disponibles desde ese estado (y, habiendo informado eso, ¿no recuerdas nada sobre el estado en cuestión? A menos que haya algo que ganar al hacer que el objeto que se le pregunta sobre los movimientos disponibles retenga un recuerdo del estado del juego, no es necesario que persista una referencia.
Es posible que en algunos casos haya ventajas al mantener el estado de mantenimiento del objeto de evaluación de reglas. Si cree que tal situación puede surgir, sugeriría agregar una clase de "árbitro" y que el libro de reglas proporcione un método de "crear árbitro". A diferencia del libro de reglas, que no le importa si se le pregunta sobre un juego, o cincuenta, un objeto árbitro esperaría oficiar un juego. No se esperaría que encapsulara todo el estado relacionado con el juego que está arbitrando, pero podría almacenar en caché cualquier información sobre el juego que considere útil. Si un juego admite la funcionalidad de "deshacer", puede ser útil que el árbitro incluya un medio para producir un objeto de "instantánea" que podría almacenarse junto con los estados anteriores del juego; ese objeto debería,
Si pudiera ser necesario algún acoplamiento entre los aspectos de procesamiento de reglas y procesamiento de estado de juego del código, el uso de un objeto árbitro permitirá mantener dicho acoplamiento fuera del libro de reglas principal y de las clases de estado de juego. También puede permitir que nuevas reglas consideren aspectos del estado del juego que la clase de estado del juego no consideraría relevante (por ejemplo, si se agregó una regla que dice "El objeto X no puede hacer Y si alguna vez ha estado en la ubicación Z ", se podría cambiar el árbitro para realizar un seguimiento de qué objetos han estado en la ubicación Z sin tener que cambiar la clase de estado del juego).
fuente
La forma correcta de lidiar con esto es usar interfaces. En lugar de que dos clases se conozcan entre sí, haga que cada clase implemente una interfaz y haga referencia a eso en la otra clase. Digamos que tiene clase A y clase B que necesitan referenciarse entre sí. Haga que la clase A implemente la interfaz A y la clase B implemente la interfaz B, entonces puede hacer referencia a la interfaz B desde la clase A y la interfaz A desde la clase B. La clase A puede estar en su propio proyecto, al igual que la clase B. Las interfaces están en un proyecto separado que los otros dos proyectos hacen referencia.
fuente