Qual è il modo corretto di modellare questa attività del mondo reale che sembra aver bisogno di riferimenti circolari in OOP?


24

Ho lottato con un problema in un progetto Java sui riferimenti circolari. Sto cercando di modellare una situazione del mondo reale in cui sembra che gli oggetti in questione siano interdipendenti e debbano conoscersi.

Il progetto è un modello generico di gioco da tavolo. Le classi di base non sono specifiche, ma sono estese per trattare specifiche di scacchi, backgammon e altri giochi. L'ho codificato in applet 11 anni fa con una mezza dozzina di giochi diversi, ma il problema è che è pieno di riferimenti circolari. L'ho implementato allora riempiendo tutte le classi intrecciate in un singolo file di origine, ma ho l'idea che questa sia una cattiva forma in Java. Ora voglio implementare una cosa simile come un'app Android e voglio fare le cose in modo corretto.

Le lezioni sono:

  • RuleBook: un oggetto che può essere interrogato per cose come il layout iniziale del tabellone, altre informazioni sullo stato del gioco iniziale come chi si muove per primo, le mosse disponibili, cosa succede allo stato di gioco dopo una mossa proposta e una valutazione di una posizione del consiglio attuale o proposta.

  • Tavola: una semplice rappresentazione di una tavola da gioco, che può essere incaricata di riflettere una mossa.

  • MoveList: un elenco di mosse. Questo è duplice: una scelta di mosse disponibili in un determinato punto o un elenco di mosse che sono state fatte nel gioco. Potrebbe essere suddiviso in due classi quasi identiche, ma non è rilevante per la domanda che sto ponendo e potrebbe complicarla ulteriormente.

  • Mossa: una sola mossa. Include tutto ciò che riguarda la mossa come un elenco di atomi: raccogli un pezzo da qui, mettilo lì sotto, rimuovi un pezzo catturato da lì.

  • Stato: le informazioni complete sullo stato di un gioco in corso. Non solo la posizione del consiglio di amministrazione, ma una MoveList e altre informazioni sullo stato come chi deve spostarsi ora. Negli scacchi si registra se il re e le torri di ciascun giocatore sono stati spostati.

I riferimenti circolari abbondano, ad esempio: il RuleBook deve conoscere lo stato del gioco per determinare quali mosse sono disponibili in un determinato momento, ma lo stato del gioco deve interrogare il RuleBook per il layout iniziale iniziale e per quali effetti collaterali accompagnano una mossa una volta è fatto (ad es. chi si sposta dopo).

Ho provato a organizzare la nuova serie di classi in modo gerarchico, con RuleBook in cima perché ha bisogno di sapere tutto. Ciò comporta la necessità di spostare molti metodi nella classe RuleBook (come fare una mossa) rendendolo monolitico e non particolarmente rappresentativo di ciò che dovrebbe essere un RuleBook.

Allora, qual è il modo corretto di organizzare questo? Devo trasformare RuleBook in BigClassThatDoesAlmostEverythingInTheGame per evitare riferimenti circolari, abbandonando il tentativo di modellare accuratamente il gioco del mondo reale? O dovrei attenermi alle classi interdipendenti e convincere il compilatore a compilarle in qualche modo, mantenendo il mio modello del mondo reale? O c'è qualche ovvia struttura valida che mi manca?

Grazie per l'aiuto che puoi dare!


7
E se RuleBookprendesse ad esempio il Statecome argomento e restituisse il valido MoveList, ovvero "ecco dove siamo ora, cosa si può fare dopo?"
jonrsharpe,

Cosa ha detto @jonrsharpe. Quando si gioca a un vero gioco da tavolo, anche il libro delle regole non è a conoscenza di alcun gioco reale giocato. Probabilmente introdurrei anche un'altra classe per calcolare effettivamente le mosse, ma ciò potrebbe dipendere da quanto è già grande questa classe RuleBook.
Sebastiaan van den Broek,

4
Evitare l'oggetto god (BigClassThatDoesAlmostEverythingInTheGame) è molto più importante che evitare riferimenti circolari.
user281377,

2
@ user281377 non sono necessariamente obiettivi reciprocamente esclusivi!
jonrsharpe,

1
Puoi mostrare i tentativi di modellazione? Un diagramma per esempio?
Utente

Risposte:


47

Ho affrontato un problema in un progetto Java sui riferimenti circolari.

Il Garbage Collector di Java non si basa sulle tecniche di conteggio dei riferimenti. I riferimenti circolari non causano alcun tipo di problema in Java. Il tempo impiegato per eliminare i riferimenti circolari perfettamente naturali in Java è tempo perso.

L'ho codificato [...] ma il problema è che è pieno di riferimenti circolari. L'ho implementato allora riempiendo tutte le classi intrecciate in un singolo file sorgente , [...]

Non necessario. Se compili tutti i file sorgente contemporaneamente (ad es. javac *.java), Il compilatore risolverà tutti i riferimenti diretti senza problemi.

O dovrei attenermi alle classi interdipendenti e convincere il compilatore a compilarle in qualche modo, [...]

Sì. Le classi di applicazione dovrebbero essere interdipendenti. Compilare tutti i file sorgente Java che appartengono allo stesso pacchetto in una volta non è un trucco intelligente, è esattamente il modo in cui si suppone che funzioni Java .


24
"I riferimenti circolari non causano alcun tipo di problema in Java." In termini di compilazione, questo è vero. Tuttavia, i riferimenti circolari sono considerati cattivi design .
Taglia

22
I riferimenti circolari sono perfettamente naturali in molte situazioni, ecco perché Java e altri linguaggi moderni usano un sofisticato garbage collector invece di un semplice contatore di riferimenti.
user281377,

3
Java è in grado di risolvere riferimenti circolari è fantastico, ed è sicuramente vero che sono naturali in molte situazioni. Ma l'OP ha presentato una situazione specifica , che dovrebbe essere presa in considerazione. Il codice spaghetti aggrovigliati non è probabilmente il modo migliore per gestire questo problema.
Matteo Leggi

3
Si prega di non diffondere FUD non comprovato su linguaggi di programmazione non correlati. Python supporta GC dei cicli di riferimento da secoli ( documenti , anche su SO: qui e qui ).
Christian Aichinger,

2
IMHO questa risposta è solo mediocre, dal momento che non c'è una sola parola sui riferimenti circolari che può essere utile per esempio del PO.
Doc Brown,

22

Le dipendenze circolari concesse sono una pratica discutibile da un punto di vista progettuale, ma non sono proibite e da un punto di vista puramente tecnico non sono nemmeno necessariamente problematiche , come sembra che le consideri: sono perfettamente legali in nella maggior parte degli scenari, sono inevitabili in alcune situazioni e in alcune rare occasioni possono persino essere considerati una cosa utile da avere.

In realtà, ci sono pochissimi scenari in cui il compilatore Java negherà una dipendenza circolare. (Nota: potrebbe essercene di più, posso solo pensare a quanto segue in questo momento.)

  1. In eredità: non puoi avere una classe A che estenda la classe B che a sua volta estende la classe A, ed è perfettamente ragionevole che non puoi averla, poiché l'alternativa non avrebbe assolutamente senso da un punto di vista logico.

  2. Tra le classi metodo-locali: le classi dichiarate all'interno di un metodo non possono fare riferimento circolare l'un l'altro. Questo probabilmente non è altro che una limitazione del compilatore Java, probabilmente perché la capacità di fare una cosa del genere non è abbastanza utile da giustificare la complessità aggiuntiva che dovrebbe andare nel compilatore per supportarlo. (La maggior parte dei programmatori Java non è nemmeno a conoscenza del fatto che è possibile dichiarare una classe all'interno di un metodo, per non parlare di dichiarare più classi, e quindi avere queste classi che si riferiscono in modo circolare tra loro.)

Pertanto, è importante rendersi conto che la ricerca di minimizzare le dipendenze circolari è una ricerca di purezza del design, non una ricerca di correttezza tecnica.

Per quanto ne so, non esiste un approccio riduzionista all'eliminazione delle dipendenze circolari, il che significa che non esiste una ricetta consistente in nient'altro che semplici passaggi "senza cervello" predeterminati per prendere un sistema con riferimenti circolari, applicarli uno dopo l'altro e terminare con un sistema privo di riferimenti circolari. Devi mettere la tua mente al lavoro e devi eseguire passaggi di refactoring che dipendono dalla natura del tuo design.

Nella particolare situazione che hai a portata di mano, mi sembra che ciò di cui hai bisogno sia una nuova entità, forse chiamata "Gioco" o "GameLogic", che conosce tutte le altre entità, (senza che nessuna delle altre entità lo sappia, ) in modo che le altre entità non debbano conoscersi.

Ad esempio, mi sembra irragionevole che la tua entità RuleBook abbia bisogno di sapere qualsiasi cosa sull'entità GameState, perché un libro delle regole è qualcosa che consultiamo per giocare, non è qualcosa che prende parte attiva al gioco. Quindi, è questa nuova entità "Gioco" che deve consultare sia il libro delle regole che lo stato del gioco per determinare quali mosse sono disponibili e questo elimina le dipendenze circolari.

Ora, penso di poter indovinare quale sarà il tuo problema con questo approccio: codificare l'entità "Gioco" in modo agnostico sarà molto difficile, quindi molto probabilmente finirai con non solo uno ma due entità che dovranno avere implementazioni personalizzate per ogni diverso tipo di gioco: l'entità "RuleBook" e "Gioco". Che a sua volta sconfigge lo scopo di avere un'entità "RuleBook" in primo luogo. Bene, tutto ciò che posso dire a riguardo è che forse, forse solo, la tua aspirazione iniziale di scrivere un sistema che può giocare a molti tipi diversi di giochi potrebbe essere stata nobile, ma forse mal concepita. Se fossi nei tuoi panni mi sarei concentrato sull'utilizzo di un meccanismo comune per visualizzare lo stato di tutti i diversi giochi e un meccanismo comune per ricevere l'input dell'utente per tutti questi giochi,


1
Grazie Mike. Hai ragione sugli svantaggi dell'entità del gioco; con il vecchio codice dell'applet sono stato in grado di elaborare nuovi giochi con poco più di una nuova sottoclasse RuleBook e il design grafico appropriato.
Damian Walker,

10

La teoria dei giochi considera i giochi come un elenco di mosse precedenti (tipi di valore incluso chi li ha giocati) e una funzione ValidMoves (previousMoves)

Vorrei provare a seguire questo schema per la parte non UI del gioco e trattare cose come la configurazione della scheda come mosse.

l'interfaccia utente può quindi essere roba OO standard con riferimento unidirezionale alla logica


Aggiornamento per condensare i commenti

Considera gli scacchi. Le partite a scacchi sono comunemente registrate come liste di mosse. http://en.wikipedia.org/wiki/Portable_Game_Notation

l'elenco delle mosse definisce lo stato completo del gioco molto meglio di un'immagine del tabellone.

Supponiamo ad esempio che iniziamo a creare oggetti per Board, Piece, Move etc e metodi come Piece.GetValidMoves ()

per prima cosa vediamo che dobbiamo avere un riferimento pezzo alla scacchiera, ma poi consideriamo il castling. cosa che puoi fare solo se non hai già spostato il tuo re o torre. Quindi abbiamo bisogno di una bandiera MovedAl già sul re e sui corvi. Allo stesso modo i pedoni possono muovere 2 quadrati alla loro prima mossa.

Quindi vediamo che nel castling la mossa valida del re dipende dall'esistenza e dallo stato della torre, quindi la tavola deve avere dei pezzi su di essa e fare riferimento a quei pezzi. stiamo entrando nel tuo problema di riferimento circolare.

Tuttavia, se definiamo Move come una struttura immutabile e lo stato di gioco come l'elenco delle mosse precedenti, scopriamo che questi problemi svaniscono. Per vedere se il castling è valido, possiamo controllare l'elenco delle mosse dell'esistenza di mosse castello e re. Per vedere se il pedone può diventare en-passent, possiamo verificare se l'altro pedone ha fatto una doppia mossa prima. Non sono necessari riferimenti ad eccezione di Regole -> Sposta

Ora gli scacchi hanno una scheda statica e i peices sono sempre impostati allo stesso modo. Ma supponiamo di avere una variante in cui consentiamo una configurazione alternativa. forse omettendo alcuni pezzi come un handicap.

Se aggiungiamo le mosse di installazione come mosse, "dalla casella al quadrato X" e adattiamo l'oggetto Regole per capire quella mossa, allora possiamo ancora rappresentare il gioco come una sequenza di mosse.

Allo stesso modo se nel tuo gioco il tabellone stesso non è statico, supponiamo che possiamo aggiungere quadrati agli scacchi o rimuovere quadrati dal tabellone in modo che non possano essere spostati. Queste modifiche possono anche essere rappresentate come mosse senza cambiare la struttura generale del motore delle regole o fare riferimento a un oggetto BoardSetup simile


Ciò tenderà a complicare l'implementazione di ValidMoves, che rallenterà la tua logica.
Taemyr,

non proprio, suppongo che l'impostazione della scheda sia variabile, quindi è necessario definirla in qualche modo. Se si convertono i passaggi di installazione in qualche altra struttura o oggetto per facilitare il calcolo, è possibile memorizzare nella cache il risultato, se necessario. Alcuni giochi hanno delle schede che cambiano con il gioco e alcune mosse valide possono dipendere dalle mosse precedenti piuttosto che dalla posizione corrente (es. Castling negli scacchi)
Ewan

1
L'aggiunta di bandiere e cose è la complessità che eviti solo con la cronologia delle mosse. non è costoso ripetere 100 mosse di scacchi per ottenere l'attuale configurazione della scacchiera e puoi memorizzare il risultato tra le mosse
Ewan

1
eviti anche di modificare il modello a oggetti per riflettere le regole. vale a dire per gli scacchi, se si effettuano movimenti validi -> Pezzo + tavola, si fallisce il castling, l'in-passent, la prima mossa per i pedoni e la promozione del pezzo e si devono aggiungere ulteriori informazioni agli oggetti o fare riferimento a un terzo oggetto. Perdi anche l'idea di chi è e di concetti come il controllo scoperto
Ewan,

1
@Gabe The boardLayoutè una funzione di tutte priorMoves(cioè se lo mantenessimo come stato, nulla di diverso sarebbe contribuito da ciascuno thisMove). Quindi il suggerimento di Ewan è essenzialmente "tagliare l'uomo di mezzo" - le mosse valide sono una funzione diretta di tutti i precedenti, anziché validMoves( boardLayout( priorMoves ) ).
OJFord,

8

Il modo standard di rimuovere un riferimento circolare tra due classi nella programmazione orientata agli oggetti consiste nell'introdurre un'interfaccia che può quindi essere implementata da una di esse. Quindi, nel tuo caso, potresti fare RuleBookriferimento a Statecui quindi si riferisce a un InitialPositionProvider(che sarebbe un'interfaccia implementata da RuleBook). Ciò semplifica anche i test, poiché è quindi possibile creare un oggetto Stateche utilizza una posizione iniziale diversa (presumibilmente più semplice) a scopo di test.


6

Credo che i riferimenti circolari e l'oggetto divino nel tuo caso possano essere facilmente rimossi separando il controllo del flusso di gioco dallo stato e dai modelli di regole del gioco. In questo modo otterresti probabilmente molta flessibilità e ti libererai di una complessità inutile.

Penso che dovresti avere un controller ("un maestro di gioco" se lo desideri) che controlla il flusso del gioco e gestisce i cambiamenti di stato effettivi invece di dare al libro delle regole o allo stato del gioco questa responsabilità.

Un oggetto dello stato del gioco non deve cambiare se stesso o essere consapevole delle regole. La classe deve solo fornire un modello di oggetti di stato del gioco facilmente gestibili (creati, ispezionati, alterati, persistenti, registrati, copiati, memorizzati nella cache ecc.) Per il resto dell'applicazione.

Il libro delle regole non dovrebbe essere necessario conoscere o giocherellare con qualsiasi gioco in corso. Dovrebbe solo avere una visione di uno stato di gioco per poter dire quali mosse sono legali e deve solo rispondere con uno stato di gioco risultante quando gli viene chiesto cosa succede quando una mossa viene applicata a uno stato di gioco. Potrebbe anche fornire uno stato iniziale del gioco quando viene richiesto un layout iniziale.

Il controller deve essere consapevole degli stati del gioco e del libro delle regole e forse di alcuni altri oggetti del modello di gioco, ma non dovrebbe aver bisogno di confondere con i dettagli.


4
ESATTAMENTE il mio pensiero. L'OP sta mescolando troppi dati e procedure nelle stesse classi. È meglio dividerli di più. Questa è una bella chiacchierata sull'argomento. A proposito, quando leggo "view to a game state", penso "argomento della funzione". +100 se potessi.
jpmc26,

5

Penso che il problema qui sia che non hai fornito una descrizione chiara di quali compiti devono essere gestiti da quali classi. Descriverò ciò che penso sia una buona descrizione di ciò che ogni classe dovrebbe fare, quindi fornirò un esempio di codice generico che illustra le idee. Vedremo che il codice è meno accoppiato e quindi non ha riferimenti circolari.

Cominciamo descrivendo cosa fa ogni classe.

La GameStateclasse dovrebbe contenere solo informazioni sullo stato attuale del gioco. Non dovrebbe contenere alcuna informazione su ciò che gli stati passati del gioco o quali mosse future sono possibili. Dovrebbe contenere solo informazioni su quali pezzi si trovano su quali quadrati negli scacchi, o quanti e che tipo di pedine sono su quali punti nel backgammon. L' GameStatedovrà contenere alcune informazioni aggiuntive, come le informazioni relative arrocco negli scacchi o circa il cubo del raddoppio in backgammon.

La Movelezione è un po 'complicata. Direi che posso specificare una mossa da giocare specificando GameStateche risulta dalla riproduzione della mossa. Quindi puoi immaginare che una mossa possa essere implementata come a GameState. Tuttavia, in go (per esempio) puoi immaginare che è molto più facile specificare una mossa specificando un singolo punto sulla scacchiera. Vogliamo che la nostra Moveclasse sia abbastanza flessibile da gestire uno di questi casi. Pertanto, la Moveclasse sarà effettivamente un'interfaccia con un metodo che accetta un pre-spostamento GameStatee restituisce un nuovo post-spostamento GameState.

Ora la RuleBookclasse è responsabile di sapere tutto sulle regole. Questo può essere suddiviso in tre cose. Deve sapere qual è l'iniziale GameState, deve sapere quali mosse sono legali e deve essere in grado di dire se uno dei giocatori ha vinto.

Potresti anche fare una GameHistorylezione per tenere traccia di tutte le mosse che sono state fatte e di tutto GameStatesciò che è accaduto. È necessaria una nuova classe perché abbiamo deciso che un singolo GameStatenon dovrebbe essere responsabile della conoscenza di tutti GameStatei precedenti.

Questo conclude le classi / interfacce che discuterò. Hai anche una Boardlezione. Ma penso che le schede in diversi giochi siano abbastanza diverse da rendere difficile capire cosa si potrebbe fare genericamente con le schede. Ora continuerò a fornire interfacce generiche e implementare classi generiche.

Il primo è GameState. Poiché questa classe dipende completamente dal gioco specifico, non esiste Gamestateun'interfaccia o classe generica .

Il prossimo è Move. Come ho detto, questo può essere rappresentato con un'interfaccia che ha un singolo metodo che prende uno stato pre-spostamento e produce uno stato post-spostamento. Ecco il codice per questa interfaccia:

package boardgame;

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

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Si noti che esiste un parametro di tipo. Questo perché, ad esempio, è ChessMovenecessario conoscere i dettagli della pre-mossa ChessGameState. Quindi, per esempio, la dichiarazione di classe di ChessMovesarebbe

class ChessMove extends Move<ChessGameState>,

dove avresti già definito una ChessGameStateclasse.

Successivamente parlerò della RuleBookclasse generica . Ecco il codice:

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

}

Di nuovo c'è un parametro di tipo per la GameStateclasse. Poiché RuleBooksi suppone che sappia qual è lo stato iniziale, abbiamo messo un metodo per dare lo stato iniziale. Poiché RuleBooksi suppone che sappiano quali mosse sono legali, disponiamo di metodi per verificare se una mossa è legale in un determinato stato e per fornire un elenco di mosse legali per un determinato stato. Infine, esiste un metodo per valutare GameState. Si noti che RuleBookdovrebbe essere responsabile solo di descrivere se uno o gli altri giocatori hanno già vinto, ma non chi si trova in una posizione migliore nel mezzo di una partita. Decidere chi è in una posizione migliore è una cosa complicata che dovrebbe essere spostata nella sua classe. Pertanto la StateEvaluationclasse è in realtà solo un semplice enum dato come segue:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Infine, descriviamo la GameHistoryclasse. Questa classe è responsabile per ricordare tutte le posizioni raggiunte nel gioco e le mosse giocate. La cosa principale che dovrebbe essere in grado di fare è registrare un Movecome giocato. Puoi anche aggiungere funzionalità per annullare Movei messaggi di posta elettronica . Ho un'implementazione di seguito.

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, potremmo immaginare di creare una Gameclasse per legare tutto insieme. Questa Gameclasse dovrebbe esporre metodi che consentano alle persone di vedere qual è la corrente GameState, vedere chi, se qualcuno ne ha uno, vedere quali mosse possono essere giocate e giocare una mossa. Ho un'implementazione di seguito

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

}

Si noti in questa classe che RuleBooknon è responsabile della conoscenza della corrente GameState. Questo è il GameHistorylavoro di. Quindi Gamechiede GameHistoryquale sia lo stato attuale e fornisce queste informazioni a RuleBookquando è Gamenecessario dire quali sono le mosse legali o se qualcuno ha vinto.

Comunque, il punto di questa risposta è che una volta che hai preso una ragionevole determinazione di ciò che ogni classe è responsabile, e fai ogni classe focalizzata su un piccolo numero di responsabilità, e assegni ogni responsabilità a una classe unica, quindi le classi tendono ad essere disaccoppiati e tutto diventa facile da codificare. Spero che ciò sia evidente dagli esempi di codice che ho dato.


3

Nella mia esperienza, i riferimenti circolari indicano generalmente che il tuo design non è ben congegnato.

Nel tuo progetto, non capisco perché RuleBook debba "conoscere" lo Stato. Potrebbe ricevere uno Stato come parametro di un metodo, certo, ma perché dovrebbe avere bisogno di sapere (cioè mantenere come variabile di istanza) un riferimento a uno Stato? Questo non ha senso per me. Un RuleBook non ha bisogno di "conoscere" lo stato di un particolare gioco per fare il suo lavoro; le regole del gioco non cambiano in base allo stato attuale del gioco. Quindi o l'hai progettato in modo errato o l'hai progettato correttamente ma lo stai spiegando in modo errato.


+1. Acquistate un gioco da tavolo fisico, ottenete un libro delle regole in grado di descrivere le regole senza stato.
unperson325680,

1

La dipendenza circolare non è necessariamente un problema tecnico, ma dovrebbe essere considerata un odore di codice, che di solito è una violazione del principio di responsabilità singola .

La tua dipendenza circolare deriva dal fatto che stai provando a fare troppo dal tuo Stateoggetto.

Qualsiasi oggetto con stato dovrebbe fornire solo metodi che si riferiscono direttamente alla gestione di quello stato locale. Se richiede qualcosa di più della logica più elementare, probabilmente dovrebbe essere suddiviso in uno schema più ampio. Alcune persone hanno opinioni diverse su questo, ma come regola generale, se stai facendo qualcosa di più di getter e setter sui dati, stai facendo troppo.

In questo caso, faresti meglio ad avere un StateFactory, che potrebbe sapere di un Rulebook. Probabilmente avresti un'altra classe di controller che usa il tuo StateFactoryper creare un nuovo gioco. Statesicuramente non dovrei saperlo Rulebook. Rulebookpotrebbe esserne a conoscenza in Statebase all'implementazione delle tue regole.


0

È necessario che un oggetto del regolamento sia associato a un determinato stato del gioco o avrebbe più senso disporre di un oggetto del regolamento con un metodo che, in base a uno stato del gioco, riporti quali mosse sono disponibili da tale stato (e, dopo averlo segnalato, non ricordi nulla dello stato in questione)? A meno che non ci sia qualcosa da guadagnare facendo in modo che l'oggetto a cui vengono chieste le mosse disponibili conservi un ricordo dello stato del gioco, non è necessario che persista un riferimento.

In alcuni casi è possibile che ci siano vantaggi nel mantenere lo stato dell'oggetto di valutazione delle regole. Se ritieni che possa sorgere una situazione del genere, suggerirei di aggiungere una classe "arbitro" e che il regolamento fornisca un metodo "createReferee". A differenza del regolamento, a cui non importa nulla se viene chiesto di una partita o cinquanta, un oggetto arbitro si aspetterebbe di officiare una partita. Non ci si aspetterebbe che incapsulasse tutto lo stato relativo al gioco che sta officiando, ma potrebbe memorizzare nella cache qualsiasi informazione sul gioco ritenuta utile. Se un gioco supporta la funzionalità "annulla", può essere utile che l'arbitro includa un mezzo per produrre un oggetto "istantanea" che può essere memorizzato insieme agli stati di gioco precedenti; quell'oggetto dovrebbe,

Se potrebbe essere necessario un certo accoppiamento tra gli aspetti di elaborazione delle regole e di elaborazione dello stato di gioco del codice, l'utilizzo di un oggetto arbitro consentirà di mantenere tale accoppiamento al di fuori del libro delle regole principale e delle classi dello stato di gioco. Può anche rendere possibile che nuove regole considerino aspetti dello stato del gioco che la classe di stato del gioco non considererebbe rilevanti (ad esempio se fosse aggiunta una regola che diceva "L'oggetto X non può fare Y se è mai stato in posizione Z ", l'arbitro potrebbe essere cambiato per tenere traccia di quali oggetti sono stati nella posizione Z senza cambiare la classe dello stato di gioco).


-2

Il modo corretto di gestirlo è utilizzare le interfacce. Invece di avere due classi che conoscono l'un l'altro, ogni classe deve implementare un'interfaccia e fare riferimento a quella nell'altra classe. Supponiamo che tu abbia classe A e classe B che devono fare riferimento a vicenda. Avere l'interfaccia A dell'attrezzo di classe A e l'interfaccia B dell'attrezzo di classe B, quindi è possibile fare riferimento all'interfaccia B della classe A e l'interfaccia A della classe B. La classe A può essere nel proprio progetto, così come la classe B. Le interfacce sono in un progetto separato a cui fanno riferimento entrambi gli altri progetti.


2
questo sembra semplicemente ripetere i punti fatti e spiegati in una risposta precedente pubblicata poche ore prima di questa
moscerino del
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.