Basso accoppiamento e forte coesione


11

Naturalmente dipende dalla situazione. Ma quando un oggetto o sistema a leva inferiore comunica con un sistema di livello superiore, è preferibile richiamare o eventi per mantenere un puntatore a un oggetto di livello superiore?

Ad esempio, abbiamo una worldclasse che ha una variabile membro vector<monster> monsters. Quando la monsterclasse comunica con il world, dovrei preferire usare una funzione di callback allora o dovrei avere un puntatore alla worldclasse all'interno della monsterclasse?


A parte l'ortografia nel titolo della domanda, in realtà non è espresso nella forma di una domanda. Penso che riformularlo potrebbe aiutare a consolidare ciò che stai chiedendo qui, dal momento che non penso che tu possa ottenere una risposta utile a questa domanda nella sua forma attuale. E non è nemmeno una domanda di progettazione del gioco, è una domanda sulla struttura di programmazione (non sono sicuro se ciò significhi che il tag di progettazione è o non è appropriato, non riesco a ricordare dove siamo arrivati ​​a "progettazione del software" e tag)
MrCranky,

Risposte:


10

Esistono tre modi principali in cui una classe può parlare con un'altra senza essere strettamente accoppiata ad essa:

  1. Attraverso una funzione di richiamata.
  2. Attraverso un sistema di eventi.
  3. Attraverso un'interfaccia

I tre sono strettamente correlati tra loro. Un sistema di eventi in molti modi è solo un elenco di callback. Un callback è più o meno un'interfaccia con un singolo metodo.

In C ++, uso raramente callback:

  1. Il C ++ non ha un buon supporto per i callback che mantengono il loro thispuntatore, quindi è difficile usare i callback nel codice orientato agli oggetti.

  2. Un callback è fondamentalmente un'interfaccia a un metodo non estendibile. Con il passare del tempo, scopro che quasi sempre ho bisogno di più di un metodo per definire quell'interfaccia e un singolo callback è raramente sufficiente.

In questo caso, probabilmente farei un'interfaccia. Nella tua domanda, non scrivi in monsterrealtà ciò a cui realmente devi comunicare world. Supponendo, farei qualcosa del tipo:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

L'idea qui è di inserire solo IWorld(che è un nome schifoso) il minimo indispensabile a cui Monsterdeve accedere. La sua visione del mondo dovrebbe essere il più stretta possibile.


1
I delegati +1 (richiamate) di solito diventano più numerosi col passare del tempo. Dare un'interfaccia ai mostri in modo che possano arrivare alle cose è un buon modo per andare secondo me.
Michael Coleman,

12

Non utilizzare una funzione di richiamata per nascondere quale funzione stai chiamando. Se dovessi inserire una funzione di callback e quella funzione di callback avrà una e una sola funzione assegnata ad essa, allora non stai affatto rompendo l'accoppiamento. Lo stai solo mascherando con un altro strato di astrazione. Non stai guadagnando nulla (tranne forse il tempo di compilazione), ma stai perdendo chiarezza.

Non la definirei esattamente una best practice, ma è un modello comune avere entità che sono contenute da qualcosa per avere un puntatore al loro genitore.

Detto questo, potrebbe valere la pena usare il modello di interfaccia per dare ai tuoi mostri un sottoinsieme limitato di funzionalità che possono fare appello al mondo.


+1 Dare al mostro un modo limitato di chiamare il genitore è una bella via di mezzo secondo me.
Michael Coleman,

7

In genere cerco di evitare i collegamenti bidirezionali, ma se devo averli, mi assicuro assolutamente che ci sia un metodo per crearli e uno per romperli, in modo da non avere mai incoerenze.

Spesso è possibile evitare completamente il collegamento bidirezionale trasmettendo i dati quando è necessario. Un banale refactoring è di fare in modo che invece di avere il mostro che mantiene un collegamento con il mondo, si passa il mondo in riferimento ai metodi mostruosi che ne hanno bisogno. Meglio ancora è solo passare in un'interfaccia per le parti del mondo di cui il mostro ha strettamente bisogno, il che significa che il mostro non arriva a fare affidamento sull'attuazione concreta del mondo. Ciò corrisponde al principio di segregazione dell'interfaccia e al principio di inversione di dipendenza , ma non inizia a introdurre l'astrazione in eccesso che a volte puoi ottenere con eventi, segnali + slot, ecc.

In un certo senso, puoi sostenere che l'uso di un callback è una mini-interfaccia molto specializzata, e va bene. Devi decidere se puoi raggiungere in modo più significativo i tuoi obiettivi tramite una raccolta di metodi in un oggetto interfaccia o diversi assortiti in callback diversi.


3

Cerco di evitare che gli oggetti contenuti chiamino il loro contenitore perché trovo che crei confusione, diventa troppo facile da giustificare, diventa abusato e crea dipendenze che non possono essere gestite.

A mio avviso, la soluzione ideale è che le classi di livello superiore siano abbastanza intelligenti da gestire le classi di livello inferiore. Ad esempio, il mondo che sa determinare se si è verificata una collisione tra un mostro e un cavaliere senza sapere dell'altro è anche meglio di me che il mostro chiede al mondo se si è scontrato con un cavaliere.

Un'altra opzione nel tuo caso, probabilmente capirei perché la classe di mostri deve conoscere la classe mondiale e molto probabilmente scoprirai che c'è qualcosa nella classe mondiale che può essere suddiviso in una classe a sé stante senso che la classe dei mostri deve conoscere.


2

Non andrai molto lontano senza eventi, ovviamente, ma prima ancora di iniziare a scrivere (e progettare) un sistema di eventi, dovresti farti la vera domanda: perché il mostro dovrebbe comunicare con la classe mondiale? Dovrebbe davvero?

Prendiamo una situazione "classica", un mostro che attacca un giocatore.

Il mostro sta attaccando: il mondo può benissimo identificare la situazione in cui un eroe si trova accanto a un mostro e dire al mostro di attaccare. Quindi la funzione in monster sarebbe:

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

Ma il mondo (che conosce già il mostro) non ha bisogno di essere conosciuto dal mostro. In realtà, il mostro può ignorare l'esistenza stessa della classe World, che è probabilmente migliore.

Stessa cosa quando il mostro si muove (lascio che i sottosistemi prendano la creatura e gestiscano il calcolo / l'intenzione di mossa per essa, il mostro è solo un sacco di dati, ma molte persone direbbero che questo non è vero OOP).

Il mio punto è: gli eventi (o il callback) sono fantastici, ovviamente, ma non sono l'unica risposta a tutti i problemi che dovrai affrontare.


1

Ogni volta che posso, provo a limitare la comunicazione tra oggetti a un modello di richiesta e risposta. Esiste un ordinamento parziale implicito sugli oggetti nel mio programma in modo tale che tra due oggetti A e B, ci possa essere un modo per A di chiamare direttamente o indirettamente un metodo di B o per B di chiamare direttamente o indirettamente un metodo di A , ma non è mai possibile che A e B si chiamino reciprocamente i metodi. A volte, ovviamente, si desidera avere una comunicazione a ritroso con il chiamante di un metodo. Ci sono un paio di modi in cui mi piace farlo, e nessuno dei due è callback.

Un modo è quello di includere più informazioni nel valore di ritorno della chiamata del metodo, il che significa che il codice client può decidere cosa fare con esso dopo che la procedura ha restituito il controllo.

L'altro modo è chiamare un oggetto figlio comune. Cioè, se A chiama un metodo su B e B deve comunicare alcune informazioni ad A, B chiama un metodo su C, dove A e B possono entrambi chiamare C, ma C non può chiamare A o B. L'oggetto A sarebbe quindi responsabile di ottenere le informazioni da C dopo che B restituisce il controllo ad A. Nota che questo non è fondamentalmente diverso dal primo modo che ho proposto. L'oggetto A può ancora recuperare le informazioni solo da un valore di ritorno; nessuno dei metodi dell'oggetto A viene invocato da B o C. Una variante di questo trucco è passare C come parametro al metodo, ma le restrizioni sulla relazione di C con A e B si applicano ancora.

Ora, la domanda importante è perché insisto a fare le cose in questo modo. Ci sono tre ragioni principali:

  • Mantiene i miei oggetti più liberamente accoppiati. I miei oggetti possono incapsulare altri oggetti, ma non dipenderanno mai dal contesto del chiamante e il contesto non dipenderà mai dagli oggetti incapsulati.
  • Mantiene facile ragionare sul mio flusso di controllo. È bello poter supporre che l'unico codice in grado di cambiare lo stato interno diself durante l'esecuzione di un metodo sia quel metodo e nessun altro. Questo è lo stesso tipo di ragionamento che potrebbe indurre a mettere mutex su oggetti simultanei.
  • Protegge gli invarianti sui dati incapsulati dei miei oggetti. I metodi pubblici possono dipendere dagli invarianti e tali invarianti possono essere violati se un metodo può essere chiamato esternamente mentre un altro è già in esecuzione.

Non sono contrario a tutti gli usi dei callback. In linea con la mia politica di non "chiamare mai il chiamante", se un oggetto A invoca un metodo su B e gli passa un callback, il callback potrebbe non cambiare lo stato interno di A e che include gli oggetti incapsulati da A e il oggetti nel contesto di A. In altre parole, il callback può invocare metodi solo su oggetti datigli da B. Il callback, in effetti, ha le stesse restrizioni di B.

Un'ultima parte libera da legare è che permetterò l'invocazione di qualsiasi funzione pura, indipendentemente da questo ordinamento parziale di cui ho parlato. Le funzioni pure sono un po 'diverse dai metodi in quanto non possono cambiare o dipendere dallo stato mutevole o dagli effetti collaterali, quindi non c'è da preoccuparsi che confondano le cose.


0

Personalmente? Uso solo un singleton.

Sì, ok, cattivo design, non orientato agli oggetti, ecc. Sai una cosa? Non mi interessa . Sto scrivendo un gioco, non una vetrina tecnologica. Nessuno mi assegnerà il codice. Lo scopo è quello di creare un gioco divertente e tutto ciò che si frappone sulla mia strada si tradurrà in un gioco meno divertente.

Avrai mai due mondi in esecuzione contemporaneamente? Può essere! Forse lo farai. Ma se non riesci a pensare a quella situazione in questo momento, probabilmente non lo farai.

Quindi, la mia soluzione: creare un mondo singleton. Chiama le funzioni su di esso. Fatti fare con tutto il casino. Si potrebbe passare un parametro in più per ogni singola funzione - e non fare errore, è lì che questo porta. Oppure potresti semplicemente scrivere il codice che funziona.

Farlo in questo modo richiede un po 'di disciplina per ripulire le cose quando diventa disordinato (è "quando", non "se") ma non c'è modo di impedire che il codice diventi disordinato - o hai il problema degli spaghetti, o le migliaia - problema degli estrattori di astrazione. Almeno in questo modo non stai scrivendo grandi quantità di codice non necessario.

E se decidi di non voler più un singleton, di solito è abbastanza semplice sbarazzartene. Richiede un po 'di lavoro, richiede il passaggio di un miliardo di parametri, ma questi sono parametri che dovresti comunque passare in giro.


2
Probabilmente lo definirei un po 'più brevemente: "Non abbiate paura di refactoring".
Tetrad,

I single sono cattivi! I globuli sono molto meglio. Tutte le virtù singleton che hai elencato sono le stesse per un globale. Uso i puntatori globali (in realtà funzioni globali che restituiscono riferimenti) ai miei vari sottosistemi e li inizializzo / li distruggo nella mia funzione principale. I puntatori globali evitano problemi di inizializzazione, penzoloni di singoletti durante lo strappo, costruzione non banale di singoletti, ecc. Ripeto che i singoloni sono malvagi.
deft_code

@Tetrad, molto d'accordo. È una delle migliori abilità che puoi avere.
ZorbaTHut
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.