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 de modelo así:
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;
};
(Imagine que tengo más clases de formas: triángulos, hexágonos, cada vez con sus variables de apuntalamiento y getters y setters asociados. Los problemas que enfrenté tenían 8 subclases, pero por el ejemplo me detuve en 2)
Ahora tengo una ShapeManager
instanciación y almacenamiento de 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 Square
parámetros relacionados (gracias a AbstractShape::getType()
) y propone cambiar el ancho del cuadrado. Para hacer eso, necesito una función que me permita modificar el ancho 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 mejor diseño que me evite usar dynamic_cast
e implementar un par getter / setter ShapeManager
para cada variable de subclase que pueda tener? Ya intenté usar la plantilla pero fallé .
El problema que estoy enfrentando no es realmente con formas pero con diferentes 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()
. El único momento en 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 temperaturaTakePhotoOfZone
necesita qué zona tomar en la foto, la ruta donde se guardará la foto, las dimensiones, etc.
Cuando llame execute()
, los Trabajos utilizarán la información específica que tienen para darse cuenta de la acción que se supone que deben hacer.
El único momento en que necesito usar el tipo concreto de un Trabajo es cuando completo o visualizo estas informaciones (si TakePhotoOfZone
Job
se selecciona una, se mostrará un widget que muestra y modifica los parámetros de zona, ruta y dimensiones).
Los Job
s luego se colocan en una lista de Job
s que toman el primer trabajo, lo ejecutan (llamando AbstractJob::execute()
), van al siguiente, y continúan hasta el final de la lista. (Por eso uso la herencia).
Para almacenar los diferentes tipos de parámetros utilizo a JsonObject
:
ventajas: la misma estructura para cualquier trabajo, sin Dynamic_cast al configurar o leer parámetros
problema: no puede almacenar punteros (a
Pattern
oZone
)
¿Crees que hay una mejor manera de almacenar datos?
Entonces, ¿cómo almacenarías el tipo concreto deJob
usarlo cuando tengo que modificar los parámetros específicos de ese tipo? JobManager
solo tiene una lista de AbstractJob*
.
fuente
changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)
dóndePropertyKey
puede ser una enumeración o una cadena, y "Ancho" (que significa que la llamada al emisor actualizará el valor de ancho) se encuentra entre uno de los valores permitidos.Respuestas:
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 más adecuadas para cualquier problema en particular.
El problema
En su ejemplo, la
AbstractShape
clase tiene ungetType()
método que básicamente identifica el tipo concreto. Esto generalmente es una señal de que no tienes una buena abstracción. El punto principal de la abstracción, después de todo, no es 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, para que te sientas como en casa.
Abstracciones útiles
Supongo que ha introducido el
AbstractShape
porque lo encontró ú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, solo puede depender de él
AbstractShape
. Por la misma razón, no necesita elgetType()
método, por lo que debe deshacerse de él.Otras partes de la aplicación solo funcionarán con un tipo particular de forma, por ejemplo
Rectangle
. Esas áreas no se beneficiarán de unaAbstractShape
clase, por lo que no debe usarla allí. Para pasar solo la forma correcta a estas partes, debe almacenar las formas de concreto por separado. (Puede almacenarlosAbstractShape
adicionalmente o combinarlos sobre la marcha).Minimizando el uso de concreto
No hay forma de evitarlo: necesita los 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 ellos.
¿Cómo lo logras? Por lo general, al introducir más abstracciones, que pueden o no reflejar las abstracciones existentes. Por ejemplo, su GUI realmente no necesita saber con qué tipo de forma se trata. Solo necesita saber que hay un área en la pantalla donde el usuario puede editar una forma.
Por lo tanto, define un resumen
ShapeEditView
para el que tieneRectangleEditView
eCircleEditView
implementaciones que contienen los cuadros de texto reales para ancho / alto o radio.En un primer paso, puede crear un
RectangleEditView
cada vez que cree unRectangle
y luego ponerlo en astd::map<AbstractShape*, AbstractShapeView*>
. Si prefiere crear las vistas a medida que las necesita, puede hacer lo siguiente en su lugar: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, obviamente debes eliminar la fábrica. Por supuesto, este ejemplo está demasiado simplificado, pero espero que la idea sea clara.
Elegir la opción correcta
En aplicaciones muy simples, es posible que una solución sucia (fundición) solo le brinde el máximo rendimiento.
El mantenimiento explícito de listas separadas para cada tipo de concreto es probablemente el camino a seguir si su aplicación trata principalmente con formas de concreto, pero tiene algunas partes que son universales. Aquí, tiene sentido abstraer solo en la medida en que lo requiera la funcionalidad común.
Ir todo el camino generalmente paga si tiene mucha lógica que opera en formas, y el tipo exacto de forma realmente es un detalle para su aplicación.
fuente
[rect, rectView]() { rectView.bind(rect); return rectView; }
. Por cierto, esto debería hacerse, por supuesto, en el módulo de presentación, por ejemplo, en un RectangleCreatedEventHandler.Un enfoque sería hacer que las cosas sean más generales para evitar la conversión a tipos específicos .
Podría implementar un getter / setter básico de propiedades flotantes de " dimensión " en la clase base, que establece un valor en un mapa, basado en una clave específica para el nombre de la propiedad. Ejemplo a continuación:
Luego, en su clase de administrador, debe implementar solo una función, como a continuación:
Ejemplo de uso dentro de la Vista:
Otra sugerencia:
Dado que su gerente solo expone el setter y el cálculo del perímetro (que también están expuestos por Shape), puede simplemente crear una Vista adecuada cuando crea una instancia de una clase de Shape específica. P.EJ:
fuente