Quando fermare l'eredità?


9

Una volta ho fatto una domanda su Stack Overflow sull'ereditarietà.

Ho detto che progetto il motore di scacchi alla moda OOP. Quindi eredito tutti i miei pezzi dalla classe astratta di Piece ma l'eredità continua. Fammi mostrare per codice

public abstract class Piece
{
    public void MakeMove();
    public void TakeBackMove();
}

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

I programmatori hanno trovato il mio progetto un po 'più ingegneristico e mi hanno suggerito di rimuovere le classi di pezzi colorate e mantenere il colore del pezzo come membro della proprietà come di seguito.

public abstract class Piece
{
    public Color Color { get; set; }
    public abstract void MakeMove();
    public abstract void TakeBackMove();
}

In questo modo Piece può conoscere il suo colore. Dopo l'implementazione ho visto che la mia implementazione va come di seguito.

public abstract class Pawn: Piece
{
    public override void MakeMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }

    public override void TakeBackMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }
}

Ora vedo che mantieni la proprietà color causando if istruzioni nelle implementazioni. Mi fa sentire che abbiamo bisogno di pezzi di colore specifici nella gerarchia ereditaria.

In tal caso avresti lezioni come WhitePawn, BlackPawn o andresti a progettare mantenendo la proprietà Color?

Senza aver visto un tale problema, come vorresti iniziare a progettare? Mantenere la proprietà Color o avere una soluzione ereditaria?

Modifica: voglio specificare che il mio esempio potrebbe non corrispondere completamente all'esempio della vita reale. Quindi, prima di provare a indovinare i dettagli dell'implementazione, basta concentrare la domanda.

In realtà sto semplicemente chiedendo se l'uso della proprietà Color provocherà se le dichiarazioni utilizzeranno meglio l'ereditarietà?


3
Non capisco davvero perché modifichi i pezzi in questo modo. Quando chiami MakeMove () quale mossa farà? Direi che ciò di cui hai davvero bisogno per iniziare è la modellazione dello stato della scheda e vedere quali classi hai bisogno per questo
jk.

11
Penso che il tuo malinteso di base non sia rendersi conto che negli scacchi il "colore" è davvero un attributo del giocatore piuttosto che del pezzo. Ecco perché diciamo "i neri si muovono" ecc., La colorazione è lì per identificare quale giocatore possiede il pezzo. Una pedina segue le stesse regole e restrizioni di movimento, indipendentemente dal colore, l'unica variazione è la direzione "avanti".
James Anderson,

5
quando non riesci a capire "quando fermare" l'eredità, è meglio lasciarla cadere completamente. Puoi reintrodurre l'eredità nella fase successiva di progettazione / implementazione / manutenzione, quando la gerarchia diventerà ovvia per te. Uso questo approccio, funziona come un incantesimo
moscerino del

1
Il problema più impegnativo è se creare o meno una sottoclasse per ogni tipo di pezzo. Il tipo di pezzo determina più aspetti comportamentali rispetto al colore del pezzo.
NoChance,

3
Se sei tentato di iniziare un progetto con ABC (Abstract Base Classes), fai un favore a tutti noi e non ... I casi in cui gli ABC sono effettivamente utili sono molto rari e anche allora, di solito è più utile usare un'interfaccia anziché.
Evan Plaice,

Risposte:


12

Immagina di progettare un'applicazione che contiene veicoli. Crei qualcosa del genere?

class BlackVehicle : Vehicle { }
class DarkBlueVehicle : Vehicle { }
class DarkGreenVehicle : Vehicle { }
// hundreds of other classes...

Nella vita reale, il colore è una proprietà di un oggetto (un'auto o un pezzo degli scacchi) e deve essere rappresentato da una proprietà.

Sono MakeMovee TakeBackMovediversi per neri e bianchi? Niente affatto: le regole sono le stesse per entrambi i giocatori, il che significa che quei due metodi saranno esattamente gli stessi per neri e bianchi , il che significa che stai aggiungendo una condizione in cui non ne hai bisogno.

D'altra parte, se hai WhitePawne BlackPawn, non sarò sorpreso se prima o poi scriverai:

var piece = (Pawn)this.CurrentPiece;
if (piece is WhitePawn)
{
}
else // The pice is of type BlackPawn
{
}

o anche qualcosa di simile:

public abstract class Pawn
{
    public Color Player
    {
        get
        {
            return this is WhitePawn ? Color.White : Color.Black;
        }
    }
}

// Now imagine doing the same thing for every other piece.

4
@Freshblood Penso che sarebbe meglio avere una directionproprietà e usarla per spostarsi piuttosto che usare la proprietà color. Il colore dovrebbe idealmente essere usato solo per il rendering, non per la logica.
Gyan aka Gary Buyn,

7
Inoltre, i pezzi si muovono nella stessa direzione, avanti, indietro, sinistra, destra. La differenza è da quale parte del tabellone iniziano. Su una strada, le auto in corsie opposte si muovono entrambe in avanti.
slip

1
@Blrfl Potresti avere ragione sul fatto che la directionproprietà sia un valore booleano. Non vedo cosa c'è che non va. Ciò che mi ha confuso sulla domanda fino al commento di Freshblood sopra è perché vorrebbe cambiare la logica del movimento in base al colore. Direi che è più difficile da capire perché non ha senso per il comportamento del colore. Se tutto ciò che vuoi fare è distinguere quale giocatore possiede quale pezzo potresti metterli in due raccolte separate ed evitare la necessità di una proprietà o eredità. I pezzi stessi potrebbero essere beatamente inconsapevoli del giocatore a cui appartengono.
Gyan aka Gary Buyn,

1
Penso che stiamo guardando la stessa cosa da due diverse prospettive. Se le due parti fossero rosse e blu il loro comportamento sarebbe diverso da quando sono in bianco e nero? Direi di no, che è la ragione per cui sto dicendo che il colore non è la causa di eventuali differenze comportamentali. È solo una sottile distinzione, ma è per questo che dovrei usare una directiono forse una playerproprietà anziché coloruna nella logica di gioco.
Gyan aka Gary Buyn,

1
@Freshblood white castle's moves differ from black's- come? Vuoi dire castling? Sia il castellaggio sul lato re che quello sul lato regina sono simmetrici al 100% per il bianco e nero.
Konrad Morawski,

4

Quello che dovresti chiederti è perché hai davvero bisogno di quelle affermazioni nella tua classe Pawn:

    if (this.Color == Color.White)
    {

    }
    else
    {
    }

Sono abbastanza sicuro che sarà sufficiente avere un piccolo set di funzioni come

public abstract class Piece
{
    bool DoesPieceHaveSameColor(Piece otherPiece) { /*...*/   }

    Color OtherColor{get{/*...*/}}
    // ...
}

nella tua classe Piece e implementa tutte le funzioni in Pawn(e le altre derivazioni di Piece) usando quelle funzioni, senza mai avere nessuna di quelle ifaffermazioni sopra. L'unica eccezione può essere la funzione per determinare la direzione di spostamento di una pedina in base al suo colore, poiché è specifica per le pedine, ma in quasi tutti gli altri casi non è necessario alcun codice specifico per il colore.

L'altra domanda interessante è se dovresti davvero avere sottoclassi diverse per diversi tipi di pezzi. Mi sembra ragionevole e naturale, dal momento che le regole per le mosse per ogni pezzo sono diverse e puoi sicuramente implementarlo usando diverse funzioni sovraccaricate per ogni tipo di pezzo.

Naturalmente, si potrebbe tentare di modellare questo in un modo più generico, separando le proprietà dei pezzi dalle loro regole in movimento, ma questo può finire in una classe astratta MovingRulee una gerarchia sottoclasse MovingRulePawn, MovingRuleQueen... non vorrei che avviarlo modo, dal momento che potrebbe facilmente portare a un'altra forma di ingegneria eccessiva.


+1 per indicare che il codice specifico per colore è probabilmente un no-no. Le pedine bianche e nere si comportano essenzialmente allo stesso modo, proprio come tutti gli altri pezzi.
Konrad Morawski,

2

L'ereditarietà non è utile quando l'estensione non influisce sulle capacità esterne dell'oggetto .

La proprietà Color non influisce sul modo in cui viene utilizzato un oggetto Piece . Ad esempio, tutti i pezzi potrebbero concepibilmente invocare un metodo moveForward o moveBackwards (anche se lo spostamento non è legale), ma la logica incapsulata del pezzo utilizza la proprietà Color per determinare il valore assoluto che rappresenta un movimento in avanti o indietro (-1 o + 1 sull'asse y), per quanto riguarda l'API, la superclasse e le sottoclassi sono oggetti identici (poiché espongono tutti gli stessi metodi).

Personalmente, definirei alcune costanti che rappresentano ogni tipo di pezzo, quindi utilizzare un'istruzione switch per ramificare in metodi privati ​​che restituiscono possibili mosse per "questa" istanza.


Il movimento all'indietro è illegale solo in caso di pedine. E non devono davvero sapere se sono bianchi o neri: è sufficiente (e logico) distinguerli tra avanti e indietro. La Boardclasse (ad esempio) potrebbe essere incaricata di convertire questi concetti in -1 o +1 a seconda del colore del pezzo.
Konrad Morawski,

0

Personalmente non erediterei affatto nulla Piece, perché non penso che tu guadagni nulla dal farlo. Presumibilmente un pezzo in realtà non importa come si muove, solo quali quadrati sono una destinazione valida per la sua prossima mossa.

Forse potresti creare un calcolatore di spostamento valido che conosce i modelli di movimento disponibili per un tipo di pezzo ed è in grado di recuperare un elenco di quadrati di destinazione validi, ad esempio

interface IMoveCalculator
{
    List<Square> GetValidDestinations();
}

class KnightMoveCalculator : IMoveCalculator;
class QueenMoveCalculator : IMoveCalculator;
class PawnMoveCalculator : IMoveCalculator; 
// etc.

class Piece
{
    IMoveCalculator move_calculator;
public:
    Piece(IMoveCalculator mc) : move_calculator(mc) {}
}

Ma chi determina quale pezzo ottiene quale calcolatore di spostamento? Se avessi una classe base di pezzi e avessi PawnPiece, potresti fare questa ipotesi. Ma poi a quel ritmo, il pezzo stesso potrebbe semplicemente implementare il modo in cui si muove, come progettato nella domanda originale
Steven Striga,

@WeekendWarrior - "Personalmente non erediterei nulla Piece". Sembra che Piece sia responsabile di qualcosa di più del semplice calcolo delle mosse, che è la ragione per dividere i movimenti come una preoccupazione separata. Determinare quale pezzo ottiene quale calcolatrice sarebbe una questione di iniezione di dipendenza, ad esempiovar my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell,

Questo sarebbe un buon candidato per il modello flyweight. Ci sono 16 pedine su una scacchiera e condividono tutti lo stesso comportamento . Questo comportamento potrebbe essere facilmente estratto in un singolo peso mosca. Lo stesso vale per tutti gli altri pezzi.
MattDavey,

-2

In questa risposta, suppongo che per "motore di scacchi", l'autore della domanda significhi "AI di scacchi", non semplicemente un motore di validazione delle mosse.

Penso che usare OOP per i pezzi e i loro movimenti validi sia una cattiva idea per due motivi:

  1. La forza di un motore di scacchi dipende fortemente dalla velocità con cui può manipolare le scacchiere , vedi ad esempio le rappresentazioni delle scacchiere del programma di scacchi . Considerando il livello di ottimizzazione descritto in questo articolo, non credo che una normale chiamata di funzione sia conveniente. Una chiamata a un metodo virtuale aggiungerebbe un livello di riferimento indiretto e comporterebbe una penalità delle prestazioni ancora più elevata. Il costo di OOP non è conveniente lì.
  2. I vantaggi di OOP come l'estensibilità non sono utili per i pezzi, poiché è probabile che gli scacchi non ottengano nuovi pezzi presto. Un'alternativa praticabile alla gerarchia di tipi basata sull'ereditarietà include l'uso di enum e switch. Tecnologia molto bassa e C-like, ma difficile da battere dal punto di vista delle prestazioni.

Allontanarsi da considerazioni sulle prestazioni, incluso il colore del pezzo nella gerarchia dei tipi è secondo me una cattiva idea. Ti troverai in situazioni in cui è necessario verificare se due pezzi hanno colori diversi, ad esempio per la cattura. Il controllo di questa condizione è facilmente eseguibile tramite una proprietà. Farlo usando is WhitePiece-test sarebbe più brutto in confronto.

C'è anche un'altra ragione pratica per evitare di spostare la generazione di mosse verso metodi specifici per pezzo, vale a dire che pezzi diversi condividono mosse comuni, ad esempio torri e regine. Al fine di evitare il codice duplicato, dovresti scomporre il codice comune in un metodo di base e avere un'altra chiamata costosa.

Detto questo, se è il primo motore di scacchi che stai scrivendo, consiglierei sicuramente di seguire principi di programmazione chiari ed evitare i trucchi di ottimizzazione descritti dall'autore di Crafty. Affrontare le specifiche delle regole degli scacchi (castling, promozione, en-passant) e complesse tecniche di intelligenza artificiale (hashing board, alpha-beta cut) è abbastanza impegnativo.


1
Potrebbe non essere utile scrivere il motore di scacchi più velocemente.
Sangue fresco

5
-1. Sentenze come "ipoteticamente potrebbe diventare un problema di performance, quindi è un male", sono quasi sempre pura superstizione. E l'eredità non riguarda solo l '"estensibilità" nel senso in cui la menzioni. Regole diverse di scacchi si applicano a pezzi diversi, e ogni tipo di pezzo che ha una diversa implementazione di un metodo come WhichMovesArePossibleè un approccio ragionevole.
Doc Brown,

2
Se non hai bisogno di estensibilità, è preferibile un approccio basato su switch / enum perché è più veloce di un metodo di spedizione virtuale. Un motore di scacchi è pesante per la CPU e le operazioni che vengono eseguite più spesso (e quindi critiche in termini di efficienza) eseguono mosse e le annullano. Solo un livello sopra viene elencato tutte le mosse possibili. Mi aspetto che anche questo sarà critico in termini di tempo. Il fatto è che ho programmato motori a scacchi in C. Anche senza OOP, le ottimizzazioni di basso livello sulla rappresentazione della scacchiera erano significative. Che tipo di esperienza hai per respingere la mia?
Joh,

2
@Joh: non mancherò di mancanza di rispetto per la tua esperienza, ma sono abbastanza sicuro che non è quello che l'OP stava cercando. Sebbene le ottimizzazioni di basso livello possano essere significative per diversi problemi, considero un errore renderle una base per le decisioni di progettazione di alto livello, soprattutto quando finora non si affrontano problemi di prestazioni. La mia esperienza è che Knuth aveva ragione quando diceva "L'ottimizzazione prematura è la radice di tutti i mali".
Doc Brown,

1
-1 Sono d'accordo con il doc. Il fatto è che questo "motore" di cui parla l'OP potrebbe essere semplicemente il motore di validazione per una partita a scacchi per 2 giocatori. In tal caso, pensare alle prestazioni di OOP rispetto a qualsiasi altro stile è completamente ridicolo, semplici convalide dei momenti richiederebbero millisecondi anche su un dispositivo ARM di fascia bassa. Non leggere di più nei requisiti di quelli effettivamente indicati.
Timothy Baldridge,
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.