Ereditarietà vs composizione per pezzi degli scacchi


9

Una rapida ricerca di questo scambio di stack mostra che in generale la composizione è generalmente considerata più flessibile dell'eredità, ma come sempre dipende dal progetto ecc. E ci sono momenti in cui l'ereditarietà è la scelta migliore. Voglio fare una partita a scacchi 3D in cui ogni pezzo ha una trama, possibilmente animazioni diverse e così via. In questo esempio concreto sembra che tu possa argomentare un caso per entrambi gli approcci, sbaglio?

L'ereditarietà sarebbe simile a questa (con un costruttore appropriato, ecc.)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

che certamente obbedisce al principio "is-a" mentre la composizione sarebbe simile a questa

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

È più una questione di preferenze personali o un approccio è chiaramente una scelta più intelligente dell'altro qui?

EDIT: Penso di aver imparato qualcosa da ogni risposta, quindi mi sento male per averne solo una. La mia soluzione finale trarrà ispirazione da molti dei post qui (ancora lavorando su di esso). Grazie a tutti coloro che hanno avuto il tempo di rispondere.


Credo che entrambi possano esistere fianco a fianco, questi due sono per scopi diversi, ma questi non sono esclusivi tra loro. Gli scacchi sono composti da diversi pezzi e assi, e pezzi di diverso tipo. Questi pezzi condividono alcune proprietà e comportamenti di base e hanno anche proprietà e comportamenti specifici. Quindi, secondo me, la composizione dovrebbe essere applicata su tavola e pezzi, mentre l'eredità dovrebbe essere seguita su tipi di pezzi.
Hitesh Gaur,

Mentre alcune persone potrebbero affermare che tutta l'eredità è cattiva, penso che l'idea generale sia che l'ereditarietà dell'interfaccia sia buona / buona e che l'ereditarietà dell'implementazione sia utile ma possa essere problematica. Qualunque cosa oltre un singolo livello di eredità è discutibile, nella mia esperienza. Può essere OK oltre a ciò, ma non è semplice farlo senza fare un casino contorto.
JimmyJames,

Il modello di strategia è comune nella programmazione dei giochi per un motivo. var Pawn = new Piece(howIMove, howITake, whatILookLike)mi sembra molto più semplice, più gestibile e più gestibile di una gerarchia ereditaria.
Formica P

@AntP Questo è un buon punto, grazie!
CanISleepYet

@AntP Dagli una risposta! ... C'è molto merito in questo!
svidgen,

Risposte:


4

A prima vista, la tua risposta finge che la soluzione "composizione" non utilizza l'ereditarietà. Ma immagino che hai semplicemente dimenticato di aggiungere questo:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(e più di queste derivazioni, una per ciascuno dei sei tipi di pezzi). Ora questo assomiglia più al classico modello "Strategico" e utilizza anche l'eredità.

Sfortunatamente, questa soluzione ha un difetto: ogni Pieceora contiene le informazioni sul tipo in modo ridondante due volte:

  • all'interno della variabile membro type

  • all'interno movementComponent(rappresentato dal sottotipo)

Questa ridondanza è ciò che potrebbe davvero metterti nei guai: una semplice classe come una Piecedovrebbe fornire una "singola fonte di verità", non due fonti.

Ovviamente, potresti provare a memorizzare solo le informazioni sul tipo typee non creare classi MovementComponentsecondarie. Ma questo progetto molto probabilmente porterebbe a una " switch(type)" dichiarazione enorme nell'implementazione di GetValidMoveSquares. E questa è sicuramente una forte indicazione del fatto che l'eredità sia la scelta migliore qui .

Nota nel motivo "eredità", è abbastanza facile fornire il campo "tipo" in modo non ridondante: aggiungere un metodo virtuale GetType()di BasePiecee attuare di conseguenza in ogni classe base.

Per quanto riguarda la "promozione": sono qui con @svidgen, trovo discutibili gli argomenti presentati nella risposta di @ TheCatWhisperer .

Interpretare la "promozione" come uno scambio fisico di pezzi invece di interpretarlo come un cambiamento del tipo dello stesso pezzo mi sembra molto più naturale. Quindi implementarlo come in un modo simile - scambiando un pezzo con un altro di un tipo diverso - molto probabilmente non causerà enormi problemi - almeno non per il caso specifico degli scacchi.


Grazie per avermi detto il nome del modello, non mi ero reso conto che si chiamasse Strategia. Hai anche ragione, mi è sfuggita l'eredità della componente di movimento nella mia domanda. Il tipo è davvero ridondante però? Se voglio illuminare tutte le regine sul tabellone, sembrerebbe strano controllare quale componente del movimento avessero, inoltre avrei bisogno di lanciare il componente del movimento per vedere che tipo è che sarebbe super brutto no?
CanISleepYet

@CanISleepYet: il problema con il campo "tipo" proposto è che potrebbe divergere dal sottotipo di movementComponent. Vedi la mia modifica come fornire un campo di tipo in modo sicuro nel progetto "ereditarietà".
Doc Brown,

4

Preferirei probabilmente l'opzione più semplice a meno che tu non abbia ragioni esplicite e ben articolate o forti preoccupazioni di estensibilità e manutenzione.

L'opzione di ereditarietà è meno riga di codice e meno complessità. Ogni tipo di pezzo ha una relazione 1 a 1 "immutabile" con le sue caratteristiche di movimento. (A differenza della sua rappresentazione, che è 1 a 1, ma non necessariamente immutabile, potresti voler offrire varie "skin".)

Se interrompi questa immutabile relazione 1 a 1 tra i pezzi e il loro comportamento / movimento in più classi, probabilmente stai solo aggiungendo complessità - e non molto altro. Quindi, se stessi rivedendo o ereditando quel codice, mi aspetto di vedere buone ragioni documentate per la complessità.

Detto questo, un'opzione ancora più semplice , IMO, è quella di creare Piece un'interfaccia implementata dai singoli pezzi . Nella maggior parte delle lingue, questo è in realtà molto diverso dall'eredità , perché un'interfaccia non ti impedirà di implementare un'altra interfaccia . ... In questo caso non ottieni alcun comportamento di classe base gratuitamente: vorresti mettere un comportamento condiviso altrove.


Non compro che la soluzione di ereditarietà sia "meno righe di codice". Forse in questa classe ma non nel complesso.
JimmyJames,

@JimmyJames Davvero? ... Basta contare le righe di codice nell'OP ... certamente non l'intera soluzione, ma non un cattivo indicatore di quale soluzione sia più LOC e complessità da mantenere.
svidgen,

Non voglio mettere un punto troppo fine su questo perché è tutto nozionale a questo punto, ma sì, lo penso davvero. La mia tesi è che questo bit di codice è trascurabile rispetto all'intera applicazione. Se questo semplifica l'implementazione delle regole (discutibile), allora potresti salvare dozzine o persino centinaia di righe di codice.
JimmyJames,

Grazie, non ho pensato all'interfaccia, ma potresti avere ragione che questo è il modo migliore di procedere. Più semplice è quasi sempre meglio.
CanISleepYet

2

La cosa con gli scacchi è che le regole del gioco e del gioco sono prescritte e fissate. Qualsiasi progetto che funzioni va bene: usa quello che ti piace! Sperimenta e provali tutti.

Nel mondo degli affari, tuttavia, nulla è così rigorosamente prescritto in questo modo: le regole e i requisiti aziendali cambiano nel tempo e i programmi devono cambiare nel tempo per adattarsi. Qui è dove i dati is-a vs. has-a vs. fanno la differenza. Qui, la semplicità semplifica la manutenzione di soluzioni complesse nel tempo e la modifica dei requisiti. In generale, negli affari, dobbiamo anche occuparci della persistenza, che può coinvolgere anche un database. Quindi, abbiamo regole come non usare più classi quando una singola classe farà il lavoro e, non usare l'ereditarietà quando la composizione è sufficiente. Ma queste regole sono tutte orientate a rendere il codice mantenibile a lungo termine di fronte al cambiamento di regole e requisiti, il che non è il caso degli scacchi.

Con gli scacchi, il percorso di manutenzione a lungo termine più probabile è che il tuo programma deve diventare sempre più intelligente, il che alla fine significa che domineranno la velocità e le ottimizzazioni di archiviazione. Per questo, dovrai generalmente fare dei compromessi che sacrificano la leggibilità per le prestazioni, e quindi anche il miglior design OO alla fine andrà a lato.


Nota che ci sono stati molti giochi di successo che prendono un concetto e quindi aggiungono alcune cose nuove al mix. Se vuoi farlo in futuro con la tua partita a scacchi, dovresti effettivamente tenere conto di eventuali cambiamenti futuri.
Flater

2

Vorrei iniziare facendo alcune osservazioni sul dominio problematico (ovvero le regole degli scacchi):

  • L'insieme di mosse valide per un pezzo dipende non solo dal tipo di pezzo ma dallo stato del tabellone (il pedone può spostarsi in avanti se il quadrato è vuoto / può muoversi in diagonale se si cattura un pezzo).
  • Alcune azioni comportano la rimozione di un pezzo esistente dal gioco (promozione / acquisizione di un pezzo) o lo spostamento di più pezzi (castling).

Questi sono imbarazzanti da modellare come una responsabilità del pezzo stesso, e né eredità né composizione si sentono benissimo qui. Sarebbe più naturale modellare queste regole come cittadini di prima classe:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRuleè un'interfaccia semplice ma flessibile che consente alle implementazioni di aggiornare lo stato della scheda in qualsiasi modo richiesto per supportare mosse complesse come il castling e la promozione. Per gli scacchi standard, avresti un'implementazione per ogni tipo di pezzo, ma puoi anche inserire regole diverse in Gameun'istanza per supportare diverse varianti di scacchi.


Approccio interessante - Buon
spunto di

1

Penso che in questo caso l'eredità sarebbe la scelta più pulita. La composizione potrebbe essere un po 'più elegante, ma mi sembra un po' più forzata.

Se stavi progettando di sviluppare altri giochi che utilizzano pezzi in movimento che si comportano in modo diverso, la composizione potrebbe essere una scelta migliore, soprattutto se hai utilizzato un modello di fabbrica per generare i pezzi necessari per ogni gioco.


2
Come gestiresti la promozione usando l'eredità?
TheCatWhisperer,

4
@TheCatWhisperer Come lo gestisci quando giochi fisicamente? (Spero che sostituisca il pezzo - non ritagliate il pedone, vero !?)
svidgen,

0

che certamente obbedisce al principio "is-a"

Bene, quindi usi l'eredità. Puoi ancora comporre nella tua classe Piece ma almeno avrai una spina dorsale. La composizione è più flessibile, ma la flessibilità è sopravvalutata, in generale, e certamente in questo caso perché le possibilità che qualcuno introduca un nuovo tipo di pezzo che richiederebbe di abbandonare il modello rigido sono zero. La partita a scacchi non cambierà presto. L'ereditarietà ti dà un modello guida, qualcosa da mettere in relazione, insegnamento del codice, direzione. Composizione non tanto, le entità di dominio più importanti non si distinguono, è senza rotazione, devi capire il gioco per capire il codice.


grazie per aver aggiunto che con la composizione è più difficile ragionare sul codice
CanISleepYet,

0

Si prega di non utilizzare l'ereditarietà qui. Mentre le altre risposte hanno certamente molte gemme di saggezza, usare l'eredità qui sarebbe certamente un errore. L'uso della composizione non solo ti aiuta ad affrontare problemi che non ti aspetti, ma qui vedo già un problema che l'eredità non può gestire con garbo.

La promozione, quando una pedina viene convertita in un pezzo più prezioso, potrebbe essere un problema con l'eredità. Tecnicamente, potresti affrontarlo sostituendo un pezzo con un altro, ma questo approccio ha dei limiti. Una di queste limitazioni sarebbe il monitoraggio delle statistiche per pezzo in cui potresti non voler ripristinare le informazioni sul pezzo al momento della promozione (o scrivere il codice aggiuntivo per copiarle).

Ora, per quanto riguarda il design della composizione nella tua domanda, penso che sia inutilmente complicato. Non ho il sospetto che tu debba avere un componente separato per ogni azione e che potresti aderire a una classe per tipo di pezzo. Il tipo enum potrebbe anche non essere necessario.

Definirei una Piececlasse (come hai già fatto), così come una PieceTypeclasse. I PieceTypedefinisce classe tutti i metodi che un pedone, regina, ect deve definire, come, CanMoveTo, GetPieceTypeName, ect. Quindi le classi Pawne Queen, ect sarebbero praticamente ereditate dalla PieceTypeclasse.


3
I problemi che vedi con la promozione e la sostituzione sono banali, IMO. La separazione delle preoccupazioni praticamente impone che tale "tracciabilità per pezzo" (o qualunque altra cosa) sia gestita in modo indipendente comunque . ... Qualsiasi potenziale vantaggio offerto dalla tua soluzione è eclissato dalle preoccupazioni immaginarie che stai cercando di affrontare, IMO.
svidgen,

5
Per chiarire: ci sono senza dubbio alcuni meriti alla soluzione proposta. Ma sono difficili da trovare nella tua risposta, che si concentra principalmente su problemi che sono davvero molto facili da risolvere nella "soluzione ereditaria". ... Questo tipo di detrazione (per me comunque) da qualsiasi merito tu stia cercando di comunicare riguardo alla tua opinione.
svidgen,

1
Se Piecenon è virtuale, sono d'accordo con questa soluzione. Mentre il design della classe può sembrare un po 'più complesso in superficie, penso che l'implementazione sarà nel complesso più semplice. Le cose principali che sarebbero associate con Piecee non la PieceTypesono la sua identità e potenzialmente la sua cronologia di mosse.
JimmyJames,

1
@svidgen Un'implementazione che utilizza un pezzo composto e un'implementazione che utilizza un pezzo basato sull'eredità apparirà sostanzialmente identica a parte i dettagli descritti in questa soluzione. Questa soluzione di composizione aggiunge un singolo livello di riferimento indiretto. Non lo vedo come qualcosa che vale la pena evitare.
JimmyJames,

2
@JimmyJames Right. Tuttavia, la cronologia delle mosse di più pezzi deve essere tracciata e correlata - non qualcosa di cui ogni singolo pezzo dovrebbe essere responsabile, IMO. È una "preoccupazione separata" se ti piacciono le cose SOLID.
svidgen,

0

Dal mio punto di vista, con le informazioni fornite, non è saggio rispondere se l'eredità o la composizione dovrebbero essere la strada da percorrere.

  • Eredità e composizione sono solo strumenti nella cintura degli strumenti del progettista orientata agli oggetti.
  • È possibile utilizzare questi strumenti per progettare molti modelli diversi per il dominio del problema.
  • Poiché esistono molti di questi modelli che possono rappresentare un dominio, a seconda del punto di vista del progettista in merito ai requisiti, è possibile combinare ereditarietà e composizione in più modi per creare modelli funzionalmente equivalenti.

I tuoi modelli sono totalmente diversi, nel primo hai modellato sul concetto di pezzi degli scacchi, nel secondo sul concetto di movimento dei pezzi. Potrebbero essere funzionalmente equivalenti e sceglierei quello che rappresenta meglio il dominio e mi aiuta a ragionare più facilmente.

Inoltre, nel tuo secondo progetto, il fatto che la tua Piececlasse abbia un typecampo, rivela chiaramente che qui c'è un problema di progettazione poiché il pezzo stesso può essere di più tipi. Non sembra che questo dovrebbe probabilmente usare una sorta di eredità, dove il pezzo stesso è il tipo?

Quindi, vedi, è molto difficile discutere della tua soluzione. Ciò che conta non è se hai usato l'ereditarietà o la composizione, ma se il tuo modello riflette accuratamente il dominio del problema e se è utile ragionare su di esso e fornirne un'implementazione.

Ci vuole un po 'di esperienza per usare l'ereditarietà e la composizione in modo corretto, ma sono strumenti completamente diversi e non credo che si possa "sostituire" l'uno con l'altro, anche se posso essere d'accordo sul fatto che un sistema possa essere progettato interamente usando solo la composizione.


Fai notare che è il modello che è importante e non lo strumento. Per quanto riguarda il tipo ridondante, l'ereditarietà aiuta davvero? Ad esempio, se voglio evidenziare tutte le pedine o controllare se un pezzo era un re, non avrei bisogno di un casting?
CanISleepYet

-1

Beh, io non suggerisco nessuno dei due.

Mentre l'orientamento agli oggetti è uno strumento utile e spesso aiuta a semplificare le cose, non è l'unico strumento nella base degli strumenti.

Prendi invece in considerazione l'implementazione di una classe board, contenente un semplice bytearray.
L'interpretazione e il calcolo delle cose su di esso viene eseguito nelle funzioni membro mediante mascheramento e commutazione dei bit.

Usa il MSB per il proprietario, lasciando 7 bit per il pezzo, ma riserva zero per vuoto.
In alternativa, zero per vuoto, segno per il proprietario, valore assoluto per il pezzo.

Se lo desideri, puoi utilizzare i campi di bit per evitare il mascheramento manuale, sebbene siano insostenibili:

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};

Interessante ... e appropriato considerando che si tratta di una domanda C ++. Ma non essendo un programmatore C ++, questo non sarebbe il mio primo (o secondo o terzo ) istinto! ... Forse questa è la risposta più C ++ qui!
svidgen,

7
Questa è una microottimizzazione che nessuna applicazione reale dovrebbe seguire.
Lie Ryan,

@LieRyan Se tutto ciò che hai è OOP, tutto ha l'odore di un oggetto. E se è davvero che "micro" è anche una domanda interessante. A seconda di cosa ci fai , potrebbe essere abbastanza significativo.
Deduplicatore

Heh ... detto questo , C ++ è orientato agli oggetti. Presumo c'è un motivo il PO sta utilizzando C ++ anziché solo C .
svidgen,

3
Qui sono meno preoccupato per la mancanza di OOP, ma piuttosto offuscando il codice con bit twiddling. A meno che tu non stia scrivendo codice per Nintendo a 8 bit o non stia scrivendo algoritmi che richiedono di gestire milioni di copie dello stato della scheda, questa è una microottimizzazione precoce e sconsigliabile. Negli scenari normali, probabilmente non sarà comunque più veloce perché l'accesso ai bit non allineato richiede operazioni sui bit aggiuntive.
Lie Ryan,
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.