Design corretto per evitare l'uso di dynamic_cast?

7

Dopo aver fatto alcune ricerche, non riesco a trovare un semplice esempio per risolvere un problema che ho riscontrato spesso.

Diciamo che voglio creare una piccola applicazione in cui posso creare Square s, Circle s e altre forme, visualizzarle su uno schermo, modificare le loro proprietà dopo averle selezionate, e quindi calcolare tutti i loro perimetri .

Vorrei fare la classe modello in questo modo:

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    SHAPE_TYPE getType() const{return m_type;}
protected :
    const SHAPE_TYPE  m_type;
};

class Square : public AbstractShape
{
public:
    Square():AbstractShape(SQUARE){}
    ~Square();

    void setWidth(float w){m_width = w;}
    float getWidth() const{return m_width;}

    float computePerimeter() const{
        return m_width*4;
    }

private :
    float m_width;
};

class Circle : public AbstractShape
{
public:
    Circle():AbstractShape(CIRCLE){}
    ~Circle();

    void setRadius(float w){m_radius = w;}
    float getRadius() const{return m_radius;}

    float computePerimeter() const{
        return 2*M_PI*m_radius;
    }

private :
    float m_radius;
};

(Immaginiamo di avere più classi di forme: triangoli, esagoni, con ogni volta le loro variabili proprers e getter e setter associati.I problemi che ho affrontato hanno avuto 8 sottoclassi ma per amore dell'esempio mi sono fermato a 2)

Ora ho ShapeManager , istanziando e memorizzando tutte le forme in un array:

class ShapeManager
{
public:
    ShapeManager();
    ~ShapeManager();

    void addShape(AbstractShape* shape){
        m_shapes.push_back(shape);
    }

    float computeShapePerimeter(int shapeIndex){
        return m_shapes[shapeIndex]->computePerimeter();
    }


private :
    std::vector<AbstractShape*> m_shapes;
};

Infine, ho una vista con le caselle di selezione per cambiare ogni parametro per ogni tipo di forma. Ad esempio, quando seleziono un quadrato sullo schermo, il widget dei parametri mostra solo i parametri relativi a Square (grazie a AbstractShape::getType() ) e propone di modificare la larghezza del quadrato. Per farlo ho bisogno di una funzione che mi permetta di modificare la larghezza in ShapeManager , e questo è il modo in cui lo faccio:

void ShapeManager::changeSquareWidth(int shapeIndex, float width){
   Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
   assert(square);
   square->setWidth(width);
}

C'è un design migliore che mi eviti di usare dynamic_cast e di implementare una coppia getter / setter in ShapeManager per ogni variabile sottoclasse che posso avere? Ho già provato a utilizzare modello ma non sono riuscito .

Il problema che sto affrontando non è in realtà con le forme ma con diverso Job s per una stampante 3D (es: PrintPatternInZoneJob , TakePhotoOfZone , ecc.) con AbstractJob come loro classe base. Il metodo virtuale è execute() e non getPerimeter() . L'unica volta che ho bisogno di utilizzare l'utilizzo concreto è di riempire le informazioni specifiche richieste da un lavoro :

  • PrintPatternInZone richiede l'elenco di punti da stampare, la posizione della zona, alcuni parametri di stampa come la temperatura

  • TakePhotoOfZone ha bisogno di quale zona prendere in foto, il percorso in cui verrà salvata la foto, le dimensioni, ecc ...

Quando chiamerò execute() , i lavori useranno le informazioni specifiche che hanno per realizzare l'azione che dovrebbero fare.

L'unica volta che ho bisogno di usare il tipo concreto di un lavoro è quando compilo o visualizzo queste informazioni (se è selezionato un TakePhotoOfZone Job , un widget che mostra e modifica la zona verranno mostrati i parametri di percorso, e dimensioni).

I Job s vengono quindi inseriti in un elenco di Job s che prende il primo lavoro, lo esegue (chiamando AbstractJob::execute() ), passa al successivo, e fino alla fine dell'elenco . (Questo è il motivo per cui utilizzo l'ereditarietà).

Per memorizzare i diversi tipi di parametri Uso un JsonObject :

    Vantaggi
  • : stessa struttura per qualsiasi lavoro, nessun dynamic_cast quando si impostano o si leggono i parametri

  • problema: impossibile memorizzare puntatori (a Pattern o Zone )

Credi che ci sia un modo migliore per archiviare i dati?

Quindi come vorresti memorizzare il tipo concreto di Job per usarlo quando devo modificare i parametri specifici di quel tipo? JobManager ha solo un elenco di AbstractJob* .

    
posta ElevenJune 04.01.2018 - 11:45
fonte

2 risposte

9

Mi piacerebbe ampliare l'"altro suggerimento" di Emerson Cardoso perché ritengo che sia l'approccio corretto nel caso generale, anche se ovviamente potresti trovare altre soluzioni più adatte a qualsiasi problema specifico.

Il problema

Nel tuo esempio, la classe AbstractShape ha un metodo getType() che identifica fondamentalmente il tipo concreto. Questo è generalmente un segno che non hai una buona astrazione. Dopotutto, il punto di astrazione non è quello di preoccuparsi dei dettagli del tipo concreto.

Inoltre, nel caso in cui non ti sia familiare, dovresti leggere il Principio Aperto / Chiuso. Viene spesso spiegato con un esempio di forme, quindi ti sentirai come a casa.

Abstrazioni utili

Suppongo che tu abbia introdotto il AbstractShape perché lo hai trovato utile per qualcosa. Molto probabilmente, alcune parti della tua applicazione devono conoscere il perimetro delle forme, indipendentemente da quale sia la forma.

Questo è il posto in cui l'astrazione ha senso. Poiché questo modulo non si occupa di forme concrete, può dipendere solo da AbstractShape . Per lo stesso motivo, non ha bisogno del metodo getType() - quindi dovresti sbarazzartene.

Altre parti dell'applicazione funzioneranno solo con un tipo particolare di forma, ad es. %codice%. Queste aree non trarranno beneficio da una classe Rectangle , quindi non dovresti usarla lì. Per trasmettere solo la forma corretta a queste parti, è necessario memorizzare separatamente le forme concrete. (Puoi memorizzarli come AbstractShape in aggiunta, o combinarli al volo).

Riduzione dell'utilizzo del calcestruzzo

Non c'è modo di aggirarlo: i tipi di calcestruzzo sono necessari in alcuni punti, almeno durante la costruzione. Tuttavia, a volte è meglio mantenere l'uso di tipi concreti limitati a poche aree ben definite. Queste aree separate hanno il solo scopo di gestire i diversi tipi, mentre tutte le logiche applicative vengono tenute fuori da esse.

Come si ottiene questo? Solitamente, introducendo più astrazioni - che possono o meno rispecchiare le astrazioni esistenti. Ad esempio, la tua GUI non veramente deve sapere che tipo di forma sta trattando. Basta sapere che c'è un'area sullo schermo in cui l'utente può modificare una forma.

Quindi definisci una AbstractShape astratta per cui hai implementazioni ShapeEditView e RectangleEditView che contengono le caselle di testo effettive per larghezza / altezza o raggio.

In un primo passaggio, potresti creare un CircleEditView ogni volta che crei un RectangleEditView e poi lo metti in un Rectangle . Se preferisci creare le viste quando ne hai bisogno, puoi fare invece quanto segue:

std::map<AbstractShape*, std::function<AbstractShapeView*()>> viewFactories;
// ...
auto rect = new Rectangle();
// ...
auto viewFactory = [rect]() { return new RectangleEditView(rect); }
viewFactories[rect] = viewFactory;

In ogni caso, il codice al di fuori di questa logica di creazione non dovrà occuparsi di forme concrete. Come parte della distruzione di una forma, è necessario rimuovere la fabbrica, ovviamente. Ovviamente, questo esempio è troppo semplificato, ma spero che l'idea sia chiara.

Scelta dell'opzione giusta

In applicazioni molto semplici, potresti scoprire che una soluzione sporca (casting) ti dà solo il massimo per il tuo dollaro.

Mantenere esplicitamente elenchi separati per ogni tipo concreto è probabilmente la strada da percorrere se la tua applicazione si occupa principalmente di forme concrete, ma ha alcune parti che sono universali. Qui, ha senso astrarre solo fino a quando la funzionalità comune lo richiede.

In generale, paga in generale se hai molta logica che funziona sulle forme e il tipo esatto di forma è davvero un dettaglio della tua applicazione.

    
risposta data 04.01.2018 - 14:58
fonte
2

Un approccio sarebbe quello di rendere le cose più generiche al fine di evitare di trasmettere a tipi specifici .

Potresti implementare un getter / setter di base delle proprietà float " dimensione " nella classe base, che imposta un valore in una mappa, basato su una chiave specifica per il nome della proprietà. Esempio di seguito:

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    void setDimension(const std::string& name, float v){ m_dimensions[name] = v; }
    float getDimension() const{ return m_dimensions[name]; }

    SHAPE_TYPE getType() const{return m_type;}

protected :
    const SHAPE_TYPE  m_type;
    std::map<std::string, float> m_dimensions;
};

Quindi, nella tua classe manager devi implementare solo una funzione, come di seguito:

void ShapeManager::changeShapeDimension(const int shapeIndex, const std::string& dimension, float value){
   m_shapes[shapeIndex]->setDimension(name, value);
}

Esempio di utilizzo all'interno della vista:

ShapeManager shapeManager;

shapeManager.addShape(new Circle());
shapeManager.changeShapeDimension(0, "RADIUS", 5.678f);
float circlePerimeter = shapeManager.computeShapePerimeter(0);

shapeManager.addShape(new Square());
shapeManager.changeShapeDimension(1, "WIDTH", 2.345f);
float squarePerimeter = shapeManager.computeShapePerimeter(1);

Un altro suggerimento:

Poiché il tuo gestore espone solo il setter e il calcolo perimetrale (che sono esposti anche da Shape), puoi semplicemente creare un'istanza di una Vista appropriata quando installi una classe Shape specifica. EG:

  • Crea un'istanza di Square e SquareEditView;
  • Passa l'istanza di Square all'oggetto SquareEditView;
  • (opzionale) Invece di avere un ShapeManager, nella tua vista principale puoi ancora tenere un elenco di forme;
  • All'interno di SquareEditView, tieni un riferimento a un quadrato; questo eliminerebbe la necessità di lanciare per modificare gli oggetti.
risposta data 04.01.2018 - 12:53
fonte

Leggi altre domande sui tag