¿El diseño adecuado para evitar el uso de dynamic_cast?

7

Después de hacer algunas investigaciones, parece que no puedo encontrar un ejemplo simple para resolver un problema que encuentro a menudo.

Digamos que quiero crear una pequeña aplicación donde pueda crear Square s, Circle s y otras formas, mostrarlas en una pantalla, modificar sus propiedades después de seleccionarlas y luego calcular todos sus perímetros .

Haría la clase modelo de esta manera:

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;
};

(Imagínese que tengo más clases de formas: triángulos, hexágonos, con cada vez sus variables proprers y sus captadores y definidores asociados. Los problemas que enfrenté tenían 8 subclases pero por el bien del ejemplo, me detuve en 2)

Ahora tengo un ShapeManager , creando instancias y almacenando todas las formas en una matriz:

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;
};

Finalmente, tengo una vista con spinboxes para cambiar cada parámetro para cada tipo de forma. Por ejemplo, cuando selecciono un cuadrado en la pantalla, el widget de parámetros solo muestra los parámetros relacionados con Square (gracias a AbstractShape::getType() ) y propone cambiar el ancho del cuadrado. Para hacer eso necesito una función que me permita modificar el ancho en ShapeManager , y así es como lo hago:

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

¿Hay un diseño mejor que me evite usar dynamic_cast e implementar una pareja getter / setter en ShapeManager para cada variable de subclase que pueda tener? Ya intenté usar la plantilla pero no funcionó .

El problema al que me estoy enfrentando no es realmente con Formas sino con Job s para una impresora 3D (por ejemplo: PrintPatternInZoneJob , TakePhotoOfZone , etc.) con AbstractJob como su clase base. El método virtual es execute() y no getPerimeter() . La única vez que necesito usar un uso concreto es llenar la información específica que necesita un trabajo :

  • PrintPatternInZone necesita la lista de puntos para imprimir, la posición de la zona, algunos parámetros de impresión como la temperatura

  • TakePhotoOfZone necesita qué zona tomar en la foto, la ruta donde se guardará la foto, las dimensiones, etc. ...

Cuando luego llame a execute() , los trabajos usarán la información específica que tienen para darse cuenta de la acción que deben hacer.

La única vez que necesito usar el tipo concreto de un trabajo es cuando completo o muestro estas informaciones (si se selecciona TakePhotoOfZone Job , un widget muestra y modifica la zona , se mostrarán los parámetros de ruta y dimensiones).

Los Job s luego se colocan en una lista de Job s que toma el primer trabajo, lo ejecuta (llamando a AbstractJob::execute() ), va al siguiente, continúa y continúa hasta el final de la lista . (Es por esto que uso herencia).

Para almacenar los diferentes tipos de parámetros uso un JsonObject :

  • ventajas: la misma estructura para cualquier trabajo, no dynamic_cast al configurar o leer parámetros

  • problema: no se pueden almacenar punteros (a Pattern o Zone )

¿Cree que hay una mejor manera de almacenar datos?

Entonces ¿cómo almacenaría el tipo concreto de Job para usarlo cuando tenga que modificar los parámetros específicos de ese tipo? JobManager solo tiene una lista de AbstractJob* .

    
pregunta ElevenJune 04.01.2018 - 11:45

2 respuestas

9

Me gustaría ampliar la "otra sugerencia" de Emerson Cardoso porque creo que es el enfoque correcto en el caso general, aunque, por supuesto, puede encontrar otras soluciones que se adapten mejor a cualquier problema en particular.

El problema

En su ejemplo, la clase AbstractShape tiene un método getType() que básicamente identifica el tipo concreto. Esto es generalmente una señal de que no tienes una buena abstracción. El punto central de la abstracción, después de todo, es no tener que preocuparse por los detalles del tipo concreto.

Además, en caso de que no esté familiarizado con él, debe leer sobre el Principio Abierto / Cerrado. A menudo se explica con un ejemplo de formas, por lo que te sentirás como en casa.

Abstracciones útiles

Supongo que has introducido el AbstractShape porque lo has encontrado útil para algo. Lo más probable es que alguna parte de su aplicación necesite conocer el perímetro de las formas, independientemente de cuál sea la forma.

Este es el lugar donde la abstracción tiene sentido. Debido a que este módulo no se ocupa de formas concretas, puede depender solo de AbstractShape . Por el mismo motivo, no necesita el método getType() , por lo que debe deshacerse de él.

Otras partes de la aplicación solo funcionarán con un tipo particular de forma, por ejemplo, %código%. Esas áreas no se beneficiarán de una clase Rectangle , por lo que no debe usarla allí. Para pasar solo la forma correcta a estas partes, necesita almacenar las formas de concreto por separado. (Puede almacenarlos como AbstractShape adicionalmente, o combinarlos sobre la marcha).

Minimizar el uso concreto

No hay manera de evitarlo: se necesitan tipos de concreto en algunos lugares, al menos durante la construcción. Sin embargo, a veces es mejor mantener el uso de tipos de concreto limitados a unas pocas áreas bien definidas. Estas áreas separadas tienen el único propósito de tratar con los diferentes tipos, mientras que toda la lógica de la aplicación se mantiene fuera de ellas.

¿Cómo logras esto? Por lo general, al introducir más abstracciones, que pueden o no reflejar las abstracciones existentes. Por ejemplo, su GUI no realmente necesita saber con qué tipo de forma está tratando. Solo necesita saber que hay un área en la pantalla donde el usuario puede editar una forma.

Así que define un AbstractShape abstracto para el que tiene implementaciones ShapeEditView y RectangleEditView que contienen los cuadros de texto reales para ancho / alto o radio.

En un primer paso, puede crear un CircleEditView cada vez que cree un RectangleEditView y luego colocarlo en un Rectangle . Si prefiere crear las vistas a medida que las necesita, puede hacer lo siguiente:

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

De cualquier manera, el código fuera de esta lógica de creación no tendrá que lidiar con formas concretas. Como parte de la destrucción de una forma, es necesario eliminar la fábrica, obviamente. Por supuesto, este ejemplo está simplificado en exceso, pero espero que la idea sea clara.

Elegir la opción correcta

En aplicaciones muy simples, es posible que una solución sucia (de fundición) le ofrezca el máximo rendimiento.

Es probable que el mantenimiento de las listas separadas para cada tipo concreto sea el camino a seguir si su aplicación trata principalmente con formas concretas, pero tiene algunas partes que son universales. Aquí, tiene sentido abstraer solo en la medida en que la funcionalidad común lo requiera.

En general, paga si tiene mucha lógica que funciona con formas, y el tipo exacto de forma realmente es un detalle para su aplicación.

    
respondido por el doubleYou 04.01.2018 - 14:58
2

Un enfoque sería hacer las cosas más generales para evitar la conversión a tipos específicos .

Puede implementar un getter / setter básico de las propiedades flotantes " dimension " en la clase base, que establece un valor en un mapa, basado en una clave específica para el nombre de la propiedad. Ejemplo abajo:

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;
};

Luego, en su clase de administrador, solo necesita implementar una función, como a continuación:

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

Ejemplo de uso dentro de la 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);

Otra sugerencia:

Ya que su administrador solo expone el definidor y el cálculo del perímetro (que también están expuestos por Shape), usted podría simplemente crear una instancia de Vista apropiada cuando cree una clase de Shape específica. EG:

  • Crea una instancia de Square y EditeView;
  • Pase la instancia de Square al objeto SquareEditView;
  • (opcional) En lugar de tener un ShapeManager, en su vista principal todavía podría mantener una lista de Formas;
  • En SquareEditView, mantienes una referencia a un cuadrado; esto eliminaría la necesidad de lanzar para editar los objetos.
respondido por el Emerson Cardoso 04.01.2018 - 12:53

Lea otras preguntas en las etiquetas