Quando interrompere l'ereditarietà?

9

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

Ho detto che progettavo il motore degli scacchi in modalità OOP. Quindi eredito tutti i miei pezzi dalla classe astratta di Piece, ma l'eredità va ancora. 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 design un po 'più ingegnoso e hanno suggerito di rimuovere classi di pezzi colorati e mantenere il colore del pezzo come un 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 mantenere la proprietà color che causa le istruzioni nelle implementazioni. Mi fa pensare che abbiamo bisogno di pezzi di colore specifici nella gerarchia dell'ereditarietà.

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

Senza vedere un problema come vorresti iniziare la progettazione? Mantieni la proprietà Color o la soluzione di ereditarietà?

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

In realtà sto semplicemente chiedendo se usare la proprietà Color farà sì che le istruzioni migliori usino l'ereditarietà?

    
posta Freshblood 18.04.2012 - 10:48
fonte

5 risposte

12

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

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

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

Sono MakeMove e TakeBackMove diversi 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 i neri e i bianchi , il che significa che stai aggiungendo una condizione in cui non ti serve esso.

D'altra parte, se hai WhitePawn e BlackPawn , non mi sorprenderò se presto o tardi 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.
    
risposta data 18.04.2012 - 11:05
fonte
4

Quello che dovresti chiedere a te stesso è il motivo per cui 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 insieme di funzioni come

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

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

nella classe Piece e implementa tutte le funzioni in Pawn (e le altre derivazioni di Piece ) utilizzando tali funzioni, senza avere mai nessuna di quelle dichiarazioni if sopra. L'unica eccezione può essere la funzione per determinare la direzione di spostamento di un pedone per il suo colore, poiché questo è specifico 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 utilizzando diverse funzioni sovraccaricate per ogni tipo di pezzo.

Naturalmente, si potrebbe provare a modellarlo in un modo più generico, separando le proprietà dei pezzi dalle loro regole di spostamento, ma questo può terminare in una classe astratta MovingRule e una sottoclasse di gerarchia MovingRulePawn , MovingRuleQueen , ... Non l'avvio in questo modo, dal momento che potrebbe facilmente portare a un'altra forma di over-engineering.

    
risposta data 18.04.2012 - 15:23
fonte
2

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

dell'oggetto.

La proprietà Color non influisce sul modo in cui un oggetto Piece è usato . Ad esempio, tutti i pezzi potrebbero idealmente invocare un metodo moveForward o moveBackwards (anche se lo spostamento non è legale), ma la logica incapsulata di Piece 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 la tua API, la tua superclasse e sottoclasse sono oggetti identici (poiché espongono tutti gli stessi metodi).

Personalmente, definirei delle costanti che rappresentano ogni tipo di pezzo, quindi utilizzare un'istruzione switch per diramarti in metodi privati che restituiscono possibili mosse per l'istanza "this".

    
risposta data 18.04.2012 - 23:01
fonte
0

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

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) {}
}
    
risposta data 18.04.2012 - 12:46
fonte
-2

In questa risposta, presumo che per "motore degli scacchi", l'autore della domanda significhi "IA degli scacchi", non solo un motore di validazione delle mosse.

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

  1. La forza di un motore di scacchi dipende in larga misura dalla velocità con cui può manipolare le schede, per es. Rappresentazioni della scheda del programma di scacchi . Considerando il livello di ottimizzazione descritto in questo articolo, non penso che una chiamata di funzione normale sia conveniente. Una chiamata a un metodo virtuale aggiungerebbe un livello di riferimento indiretto e incorre in una penalizzazione 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, dato che gli scacchi non avranno probabilità di ottenere nuovi pezzi in qualunque momento. Un'alternativa valida alla gerarchia di tipi basata sull'eredità include l'utilizzo di enumerazioni e switch. Molto low-tech e C-like, ma difficile da battere in termini di prestazioni.

L'allontanamento da considerazioni sulle prestazioni, incluso il colore del pezzo nella gerarchia di tipi, a mio parere è una cattiva idea. Ti troverai in situazioni in cui è necessario verificare se due pezzi hanno colori diversi, ad esempio per l'acquisizione. Il controllo di questa condizione è fatto facilmente usando una proprietà. Farlo usando is WhitePiece -test sarebbe più brutto in confronto.

C'è anche un altro motivo pratico per evitare di spostare la generazione del movimento in metodi specifici del pezzo, vale a dire che pezzi diversi condividono le mosse comuni, ad es. torri e regine. Per evitare la duplicazione del codice, dovresti ridurre 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 puliti ed evitare i trucchi di ottimizzazione descritti dall'autore di Crafty. Trattare con le specifiche delle regole degli scacchi (arrocco, promozione, en passant) e le complesse tecniche di IA (hashing board, alfa-beta cut) è abbastanza impegnativo.

    
risposta data 18.04.2012 - 12:37
fonte