¿Cuál es la forma correcta de modelar esta actividad del mundo real que parece necesitar referencias circulares en OOP?

24

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!

Damian Walker
fuente
77
¿Qué pasa si RuleBooktomamos, por ejemplo, el Statecomo argumento, y devolvimos el válido MoveList, es decir, "aquí es donde estamos ahora, qué se puede hacer a continuación?"
jonrsharpe
Lo que dijo @jonrsharpe. Cuando se juega un juego de mesa real, el libro de reglas tampoco sabe acerca de los juegos reales que se juegan. Probablemente incluso introduciría otra clase para calcular los movimientos, pero eso podría depender de cuán grande sea esta clase de RuleBook.
Sebastiaan van den Broek
44
Evitar el objeto dios (BigClassThatDoesAlmostEverythingInTheGame) es mucho más importante que evitar las referencias circulares.
user281377
2
@ user281377, sin embargo, no son necesariamente objetivos mutuamente excluyentes.
jonrsharpe
1
¿Puedes mostrar los intentos de modelado? Un diagrama por ejemplo?
Usuario

Respuestas:

47

He estado luchando con un problema en un proyecto Java sobre referencias circulares.

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.

Codifiqué esto [...] 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 , [...]

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.

¿O debería seguir con las clases interdependientes y convencer al compilador para que las compile de alguna manera, [...]

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 .

Atsby
fuente
24
"Las referencias circulares no causan ningún tipo de problema en Java". En términos de compilación, esto es cierto. Sin embargo, las referencias circulares se consideran mal diseño .
Chop
22
Las referencias circulares son perfectamente naturales en muchas situaciones, es por eso que Java y otros lenguajes modernos usan un recolector de basura sofisticado en lugar de un simple contador de referencias.
user281377
3
La capacidad de Java para resolver referencias circulares es excelente, y definitivamente es cierto que son naturales en muchas situaciones. Pero OP presentó una situación específica , y eso debería ser considerado. El código de espagueti enredado probablemente no sea la mejor manera de manejar este problema.
Mateo leyó
3
No difunda FUD sin fundamento sobre lenguajes de programación no relacionados. Python ha soportado GC de ciclos de referencia desde hace siglos ( documentos , también en SO: aquí y aquí ).
Christian Aichinger
2
En mi humilde opinión, esta respuesta es solo mediocre, ya que no hay una palabra sobre referencias circulares que sean útiles, por ejemplo, del OP.
Doc Brown
22

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).

  1. 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.

  2. 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,

Mike Nakis
fuente
1
Gracias Mike Tienes razón sobre los inconvenientes de la entidad del Juego; Con el antiguo código de applet, he podido diseñar nuevos juegos con poco más que una nueva subclase de RuleBook y el diseño gráfico apropiado.
Damian Walker
10

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

Ewan
fuente
Esto tenderá a complicar la implementación de ValidMoves, lo que ralentizará su lógica.
Taemyr
en realidad no, supongo que la configuración de la placa es variable, por lo que debe definirla de alguna manera. Si convierte los movimientos de configuración a otra estructura u objeto para ayudar al cálculo, puede almacenar en caché el resultado si es necesario. Algunos juegos tienen tableros que cambian con el juego y algunos movimientos válidos pueden depender de movimientos anteriores en lugar de la posición actual (por ejemplo, enrocarse en el ajedrez)
Ewan
1
Agregar banderas y otras cosas es la complejidad que evita simplemente con el historial de movimientos. no es costoso recorrer más de 100 movimientos de ajedrez para obtener la configuración actual del tablero y puedes guardar el resultado entre movimientos
Ewan
1
También evita cambiar su modelo de objetos para reflejar las reglas. es decir, para el ajedrez, si realiza Movimientos válidos -> Pieza + Tablero, falla el enroque, al pasar, el primer movimiento para los peones y la promoción de piezas y tiene que agregar información adicional a los objetos o hacer referencia a un tercer objeto. También pierdes la idea de quién es y conceptos como el cheque descubierto
Ewan
1
@Gabe The boardLayoutes una función de todos priorMoves(es decir, si lo mantuviéramos como estado, no se aportaría nada más que cada uno thisMove). 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 de validMoves( boardLayout( priorMoves ) ).
OJFord
8

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 RuleBookreferencia a lo Stateque luego se refiere a un InitialPositionProvider(que sería una interfaz implementada por RuleBook). Esto también facilita las pruebas, ya que puede crear una Stateque use una posición inicial diferente (presumiblemente más simple) para fines de prueba.

Jules
fuente
6

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.

VIENE DE
fuente
44
EXACTAMENTE mi pensamiento. El OP está mezclando demasiados datos y procedimientos en las mismas clases. Es mejor dividirlos más. Esta es una buena charla sobre el tema. Por cierto, cuando leo "ver a un estado de juego", pienso "argumento a la función". +100 si pudiera.
jpmc26
5

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 GameStateclase 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. El GameStatetendrá 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 Moveclase es un poco complicada. Yo diría que puedo especificar un movimiento para jugar especificando el GameStateresultado de jugar el movimiento. Así que podrías imaginar que un movimiento solo se puede implementar como a GameState. 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 nuestra Moveclase sea lo suficientemente flexible como para manejar cualquiera de estos casos. Por lo tanto, la Moveclase realmente será una interfaz con un método que toma un movimiento previo GameStatey devuelve un nuevo movimiento posterior GameState.

Ahora la RuleBookclase es responsable de saber todo sobre las reglas. Esto se puede dividir en tres cosas. Necesita saber cuál es la inicial GameState, necesita saber qué movimientos son legales y necesita saber si uno de los jugadores ha ganado.

También puede hacer una GameHistoryclase para realizar un seguimiento de todos los movimientos que se han realizado y todo lo GameStatesque ha sucedido. Es necesaria una nueva clase porque decidimos que una sola GameStateno debería ser responsable de conocer todos los GameStatemensajes anteriores.

Esto concluye las clases / interfaces que discutiré. También tienes una Boardclase. 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 una Gamestateinterfaz 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:

package boardgame;

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

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Tenga en cuenta que hay un parámetro de tipo. Esto se debe a que, por ejemplo, ChessMoveserá necesario conocer los detalles del pre-movimiento ChessGameState. Entonces, por ejemplo, la declaración de clase de ChessMovesería

class ChessMove extends Move<ChessGameState>,

donde ya habrías definido una ChessGameStateclase.

A continuación hablaré sobre la RuleBookclase genérica . Aquí está el código:

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

}

Nuevamente hay un parámetro de tipo para la GameStateclase. Como RuleBookse supone que sabe cuál es el estado inicial, hemos puesto un método para dar el estado inicial. Dado que RuleBookse 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 el GameState. Tenga en cuenta que el RuleBooksolo 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, la StateEvaluationclase es en realidad solo una enumeración simple dada de la siguiente manera:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Por último, describamos la GameHistoryclase. 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 una Movereproducción. También puede agregar funcionalidad para deshacer Moves. Tengo una implementación a continuación.

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

}

Finalmente, podríamos imaginar hacer una Gameclase para unir todo. Se Gamesupone que esta clase expone métodos que permiten a las personas ver cuál es la corriente GameState, ver quién, si alguien tiene uno, ver qué movimientos se pueden jugar y jugar un movimiento. Tengo una implementación a continuación

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

}

Observe en esta clase que RuleBookno es responsable de saber cuál es la corriente GameState. Ese es el GameHistorytrabajo de. Entonces, Gamepregunta GameHistorycuál es el estado actual y le da esta información a RuleBookcuándo Gamenecesita 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.

Brian Moths
fuente
3

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.

Mehrdad
fuente
+1. Compras un juego de mesa físico, obtienes un libro de reglas que puede describir las reglas sin estado.
unperson325680
1

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 Stateobjeto.

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 un Rulebook. Probablemente tendrías otra clase de controlador que usa tu StateFactorypara crear un nuevo juego. Statedefinitivamente no debería saberlo Rulebook. Rulebookpodría saber acerca de una Statedependencia de la implementación de sus reglas.

00500005
fuente
0

¿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).

Super gato
fuente
-2

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.

Peter
fuente
2
esto parece simplemente repetir los puntos hechos y explicados en una respuesta anterior publicada pocas horas antes de esta
mosquito