Come evitare un downcast eccessivo quando si utilizza l'ereditarietà

3

Sto codificando un gioco e ho un problema di progettazione in cui ho una superclasse da cui ereditano tutti gli elementi del gioco. Il gioco è basato sulla griglia e gli oggetti possono essere posizionati in diverse parti della griglia. La griglia ha un dizionario di posizioni di griglia - > BASEITEM.

Il problema è che gli oggetti derivati fanno cose molto diverse. Quindi devo sempre downcast per ottenere i comportamenti specifici che voglio quando recupero roba dalla griglia. Ecco un semplice esempio:

public abstract class BaseObj {
  public string Name {get; set;}
  public int Hitpoints {get; set;}
}

public class Turret : BaseObj {
    public void FireWeapon() { }
    public int Damage {get; set;}
}

public class ProductionBuilding: BaseObj {
    public void CraftItem(BaseObj item) {}
    public float CraftSpeed {get; set;}
}

Quindi uno scenario è dove il giocatore sta selezionando un oggetto dalla griglia. La classe della griglia ha un metodo che restituisce l'oggetto situato alle coordinate del mouse quando si fa clic.

Da qui, ho bisogno di determinare che tipo di cosa il giocatore ha selezionato. Determina quali pannelli dell'interfaccia utente mostro e quali comportamenti posso fare sugli elementi. Di conseguenza la mia base di codice è piena di downcasts e typechecks. Ci deve essere un modello di progettazione che si occupa di strutture ereditarie moderatamente complesse e downcasting. Qualche idea?

Sto codificando in C # e Unity, ma immagino che la risposta sarà abbastanza generale da comprendere OOP in generale.

Questo non è un duplicato di: Come evitare il downcast continuo in questo caso?

In quel caso, l'oggetto contenitore ospitava solo un cane, quindi applicare un tipo di modello a DogHouse era abbastanza facile. Ma questa soluzione non può essere applicata alla mia griglia, poiché contiene un elenco di elementi.

EDIT:

Come richiesto, ecco un esempio di dove sto lanciando. Il contesto di questo è il giocatore sta mettendo giù un oggetto sulla griglia.

  if (typeof(StationModule) == item.GetType())
  {
    StationManager.HandleModuleAdded((StationModule)item);
  }

Il StationManager fa qualcosa di speciale quando sono posizionati StationModule, ma il giocatore può mettere altre cose oltre a StationModule sulla griglia, nel qual caso StationManager non gli interessa.

    
posta Slims 01.12.2018 - 20:32
fonte

4 risposte

9

From here, I need to determine what kind of thing the player has selected. It will determine which UI panels I show and what behaviors I can do on the items. As a result my code base is littered with downcasts and typechecks. There must be some design pattern that deals with moderately complex inheritance structures and downcasting. Any ideas?

Il modello di progettazione si chiama "metti quella roba nella classe in cui appartiene". Se la disposizione dei pannelli dell'interfaccia utente e "i comportamenti che posso fare sugli elementi" sono definiti in base al tipo dinamico dell'oggetto, le funzioni dell'interfaccia per l'esecuzione di tale compito devono essere nella classe base.

Cioè, il tuo tipo BaseObj dovrebbe avere una funzione virtuale "SetupPanels" che i tipi derivati specializzano. Spetta a te esattamente come funziona (potrebbe effettivamente creare i pannelli, o restituire una struttura dati che descrive quali pannelli creare, o qualsiasi altra cosa), ma dovrebbe essere una parte fondamentale dell'interfaccia di BaseObj , non qualcosa che vive al di fuori di BaseObj .

Fondamentalmente, la maggior parte delle volte quando inizi "downcasting" (o addirittura concepisci quella parola), allora una delle due cose sta succedendo: o non hai aggiunto la funzionalità corretta alla classe base comune, o non dovresti iniziare con l'ereditarietà.

The StationManager does some special stuff when StationModules are placed, but the player can place other things besides StationModules onto the grid, in which case StationManager does not care.

Se "posizionare" un oggetto ha bisogno di far sapere a qualcuno che sta accadendo in base a che tipo è, allora la tua classe base dovrebbe avere un membro virtuale che dia all'elemento la possibilità di dire a chiunque sia ciò che deve sapere. È un concetto che è parte integrante del fatto che è un StationModule , quindi StationModule dovrebbe saperlo.

    
risposta data 01.12.2018 - 21:40
fonte
3

Questo è un problema comune quando si utilizza un contenitore generico come una griglia per memorizzare sottotipi diversi di un oggetto. Il primo approccio quindi è sicuramente quello di verificare se la classe base contiene i metodi corretti in modo da consentire di risolvere direttamente i problemi con il polimorfismo.

Tuttavia, spesso le operazioni richieste sui diversi tipi di oggetti non possono essere facilmente implementate come funzioni polimorfiche, perché

  • sono operazioni in un tipo completamente diverso di livello (come l'interfaccia utente). L'esempio di SetupPanels nella risposta di Nicol Bolas potrebbe essere un tale metodo, quindi la soluzione spesso non è così semplice come impone quella risposta.

  • metterebbero responsabilità negli oggetti che chiaramente non appartengono a questo.

Una possibile soluzione a questo è rendere i tuoi sottotipi fornire meta informazioni su se stessi, in modo che le operazioni possano essere implementate in modo più generico. L'idea è che un livello come l'interfaccia utente possa chiedere all'oggetto le sue capacità, come quali proprietà visualizzabili, quali tipi queste proprietà hanno, ecc. Questo potrebbe essere implementato sia in termini di funzioni polimorfiche che indicano al tuo programma le funzioni / caratteristiche disponibili o (in C #) facendo uso di attributi . Ovviamente, per raccogliere quelle informazioni è necessario fare uso della riflessione.

Un altro approccio potrebbe essere quello di fornire al tuo oggetto base diversi oggetti strategici iniettabili . Ad esempio, l'oggetto base potrebbe fornire una proprietà DrawingStrategy , che viene inizializzata una volta durante la costruzione dell'oggetto. L'interfaccia astratta IDrawingStrategy potrebbe quindi avere sottotipi diversi dal livello dell'interfaccia utente . Questo va oltre il semplice dare all'oggetto di base un metodo polimorfo Draw .

Durante l'inizializzazione dell'oggetto, il programma deve quindi inserire le strategie corrette nei sottotipi corretti. Ma ciò è accettabile, poiché "la creazione di oggetti" è il posto nel tuo programma in cui i diversi sottotipi devono essere conosciuti. L'idea è di mantenere il posto unico, in modo che il tuo codice base non diventi ingombrante con controlli di tipo in luoghi arbitrari.

    
risposta data 02.12.2018 - 05:42
fonte
1

Consideriamo un dominio familiare.

Stiamo mostrando una finestra con due parti. Il primo mostra un layout dell'interfaccia utente. Per semplicità, assumiamo che ci siano solo tre elementi dell'interfaccia utente consentiti, TextBox , Button e CheckBox ciascuno con le sue proprietà. Vengono tutti ereditati da Element . Tutti avranno dei limiti, quindi i limiti sono posizionati in Element .

class Element {
    Rect bounds;
}

class TextBox extends Element {
    String hint;
}

class Button extends Element {
    String label;
}

class CheckBox extends Element {
    boolean checked;
}

Quando viene selezionato un elemento UI, le sue proprietà corrispondenti dovrebbero essere mostrate nella seconda parte della finestra. Ad esempio, se è selezionato Button , nella seconda parte dovrebbe essere visualizzato bounds e label .

Il primo tentativo sarebbe quello di scrivere una classe PropertiesPanel come sotto.

class PropertiesPanel {

    void displayProperties(Element e) {

        if (e instanceof TextBox)
            displayTextBoxProperties((TextBox) e);
        else if (e instanceof Button)
            displayButtonProperties((Button) e);
        else if (e instanceof CheckBox)
            displayCheckBoxProperties((CheckBox) e);

    }
}

Supponi che i metodi displayTextBoxProperties() , ecc sappiano come visualizzare un singolo elemento dell'interfaccia utente.

Questa soluzione soffre del problema di fusione eccessivo. Possiamo farlo meglio Riscrivi la gerarchia Element come sotto.

abstract class Element {
    Rect bounds;
    abstract void display(PropertiesPanel panel);
}

class TextBox extends Element {
    String hint;

    void display(PropertiesPanel panel) {
        panel.displayTextBoxProperties(this);
    }
}

class Button extends Element {
    String label;

    void display(PropertiesPanel panel) {
        panel.displayButtonProperties(this);
    }
}

class CheckBox extends Element {
    boolean checked;

    void display(PropertiesPanel panel) {
        panel.displayCheckBoxProperties(this);
    }
}

E non dimenticare di rimuovere il lungo metodo displayProperties() da PropertiesPanel . Infine nell'evento di selezione dell'elemento scrivi come sotto.

PropertiesPanel mPropertiesPanel;

void onElementSelected(Element e) {
    e.display(mPropertiesPanel);
}

Spero che questa soluzione possa essere tradotta facilmente nel tuo dominio.

Il buon nome di questo modello è Modello visitatore .

Nota: qui PropertiesPanel è il visitatore. E il visitatore è concreto e non gerarchico. Come descritto nel modello, puoi creare una gerarchia di visitatori, se necessario.

    
risposta data 04.12.2018 - 19:40
fonte
1

Inverti la direzione della chiamata

Il modo più comune per evitare downcasting è di invertire la direzione della chiamata. La griglia non sa che tipo di BaseObj è contenuto in una tessera, ma BaseObj sa esattamente quale tipo di griglia è contenuta, giusto?

Mi vengono in mente due modi per invertire la direzione.

Basato su evento

  1. Crea una classe Tile che avvolge BaseObj .

    class Tile
    {
        event ClickEventHandler OnClick;
    
        protected readonly BaseObj _gameObject;
    
        public Tile(BaseObj gameObject)
        {
            _gameObject = gameObject;
        }
    
        public void Click()
        {
            this.OnClick?.Invoke(this, new EventArgs());
        }
    
        public BaseObj GameObject => _gameObject;
    }
    
  2. Quando aggiungi l'oggetto del gioco alla griglia, avvolgilo in una nuova tessera e iscriviti ai suoi eventi. Dal momento che hai appena creato un'istanza dell'oggetto del gioco, sai di che tipo si tratta e in che modo dovresti iscriverti.

    var turret = new Turret();
    var tile = new Tile(turret);
    tile.OnClick += (s,e) => turret.FireWeapon();
    grid[x,y] = tile;
    
  3. Quando l'utente fa clic sul riquadro, gestisci quel clic chiamando il riquadro, non la torretta.

    grid[x,y].Click();
    

    In risposta, il Tile emetterà l'evento e verrà inviato al tuo gestore che sparerà con l'arma.

  4. Puoi ancora accedere all'oggetto originale tramite la proprietà GameObject :

    var itemName = grid[x,y].GameObject.Name;
    

semplice

Puoi semplicemente aggiungere un metodo Click() al tipo di base e sovrascriverlo in ogni tipo derivato. Ciò presenta una separazione dei problemi più scarsa, ma è un progetto molto semplice e richiede l'aggiunta di un solo metodo alla classe base.

abstract class BaseObj
{
    public abstract void Click();
}

class Turret : BaseObj
{
    public override void Click()
    {
        this.FireWeapon();
    }
}

Quindi, quando un utente fa clic, chiama il metodo Click() . Poiché questo è definito nel tipo di base, non è necessaria alcuna trasmissione.

Un problema che potrebbe sorgere è che non è possibile passare alcun argomento specifico del tipo a Click . Ad esempio, forse la torretta ha bisogno di un singolo argomento che indica la velocità di fuoco, ma nessun altro oggetto di gioco ne ha bisogno, quindi non ha senso inserirlo nel metodo Click della classe base. È qui che entra in gioco la direzione inversa della chiamata; l'oggetto del gioco può ancora tirare le informazioni di cui ha bisogno dalla griglia o da altro contesto.

abstract class BaseObj
{
    public abstract void Click(GameContext context);  //Common to all game objects
}

class Turret : BaseObj
{
    public override void Click(GameContext context)
    {
        this.FireWeapon(context.RateOfFire);  //Pulls only what it needs from the context
    }
}
    
risposta data 05.12.2018 - 01:00
fonte

Leggi altre domande sui tag