Depois de fazer algumas pesquisas, não consigo encontrar um exemplo simples para resolver um problema que encontro com frequência.
Digamos que eu queira criar um pequeno aplicativo em que possa criar Square
s, se Circle
outras formas, exibi-las em uma tela, modificar suas propriedades depois de selecioná-las e depois calcular todos os seus perímetros.
Eu faria a classe model como esta:
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 eu tenho mais classes de formas: triângulos, hexágonos, com cada vez que suas variáveis de proprietários e getters e setters associados. Os problemas que enfrentei tinham 8 subclasses, mas pelo exemplo parei em 2)
Agora tenho um ShapeManager
, instanciando e armazenando todas as formas em uma 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, tenho uma visualização com caixas de rotação para alterar cada parâmetro para cada tipo de forma. Por exemplo, quando eu seleciono um quadrado na tela, o widget de parâmetro exibe apenas Square
parâmetros relacionados (graças a AbstractShape::getType()
) e propõe a alteração da largura do quadrado. Para fazer isso, preciso de uma função que permita modificar a largura ShapeManager
, e é assim que eu faço:
void ShapeManager::changeSquareWidth(int shapeIndex, float width){
Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
assert(square);
square->setWidth(width);
}
Existe um design melhor que me evite usar dynamic_cast
e implementar um casal getter / setter ShapeManager
para cada variável de subclasse que eu possa ter? Eu já tentei usar o modelo, mas falhei .
O problema que estou enfrentando não é realmente com formas, mas com diferentes Job
s para uma impressora 3D (ex: PrintPatternInZoneJob
, TakePhotoOfZone
, etc.) com AbstractJob
como sua classe base. O método virtual é execute()
e não getPerimeter()
. O único momento em que preciso usar o uso concreto é preencher as informações específicas de que um trabalho precisa :
PrintPatternInZone
precisa da lista de pontos para imprimir, da posição da zona, de alguns parâmetros de impressão, como a temperaturaTakePhotoOfZone
precisa de qual zona levar na foto, o caminho onde a foto será salva, as dimensões, etc ...
Quando eu ligarei execute()
, os Jobs usarão as informações específicas que eles têm para realizar a ação que devem executar.
O único momento em que preciso usar o tipo concreto de um trabalho é quando eu preencher ou exibir essas informações (se a TakePhotoOfZone
Job
for selecionada, um widget exibindo e modificando os parâmetros de zona, caminho e dimensões será mostrado).
Os Job
s são então colocados em uma lista de Job
s que recebem o primeiro trabalho, o executam (chamando AbstractJob::execute()
), passam para o próximo, até o final da lista. (É por isso que eu uso herança).
Para armazenar os diferentes tipos de parâmetros, eu uso um JsonObject
:
vantagens: mesma estrutura para qualquer trabalho, sem dynamic_cast ao definir ou ler parâmetros
problema: não é possível armazenar ponteiros (para
Pattern
ouZone
)
Você acha que existe uma maneira melhor de armazenar dados?
Então, como você armazenaria o tipo concreto doJob
para usá-lo quando eu tiver que modificar os parâmetros específicos desse tipo? JobManager
só tem uma lista de AbstractJob*
.
fonte
changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)
wherePropertyKey
pode ser uma enumeração ou string, e "Width" (que significa que a chamada ao setter atualizará o valor da width) está entre um dos valores permitidos.Respostas:
Gostaria de expandir a "outra sugestão" de Emerson Cardoso, porque acredito que seja a abordagem correta no caso geral - embora você possa, é claro, encontrar outras soluções mais adequadas a qualquer problema em particular.
O problema
No seu exemplo, a
AbstractShape
classe possui umgetType()
método que identifica basicamente o tipo concreto. Isso geralmente é um sinal de que você não tem uma boa abstração. Afinal, o objetivo de abstrair não é ter que se preocupar com os detalhes do tipo concreto.Além disso, caso você não esteja familiarizado com isso, você deve ler o Princípio Aberto / Fechado. Muitas vezes é explicado com um exemplo de formas, para que você se sinta em casa.
Abstrações úteis
Suponho que você tenha introduzido o
AbstractShape
porque achou útil para alguma coisa. Provavelmente, alguma parte do seu aplicativo precisa conhecer o perímetro das formas, independentemente de qual seja a forma.Este é o lugar onde a abstração faz sentido. Como este módulo não se preocupa com formas concretas, ele pode depender
AbstractShape
apenas. Pelo mesmo motivo, ele não precisa dogetType()
método - portanto, você deve se livrar dele.Outras partes do aplicativo funcionarão apenas com um tipo específico de forma, por exemplo
Rectangle
. Essas áreas não se beneficiarão de umaAbstractShape
aula, portanto você não deve usá-la lá. Para passar apenas a forma correta para essas peças, é necessário armazenar formas concretas separadamente. (Você pode armazená-los comoAbstractShape
adicionalmente ou combiná-los rapidamente).Minimizando o uso do concreto
Não há como contornar: você precisa dos tipos de concreto em alguns lugares - pelo menos durante a construção. No entanto, às vezes é melhor manter o uso de tipos de concreto limitado a algumas áreas bem definidas. Essas áreas separadas têm o único objetivo de lidar com os diferentes tipos - enquanto toda a lógica do aplicativo é mantida fora deles.
Como você consegue isso? Geralmente, introduzindo mais abstrações - o que pode ou não refletir as abstrações existentes. Por exemplo, sua GUI realmente não precisa saber com que tipo de forma está lidando. Ele só precisa saber que existe uma área na tela onde o usuário pode editar uma forma.
Assim, você define um resumo
ShapeEditView
para o qual possuiRectangleEditView
eCircleEditView
implementações que mantêm as caixas de texto reais para largura / altura ou raio.Em uma primeira etapa, você pode criar um
RectangleEditView
sempre que criar umRectangle
e depois colocá-lo em umstd::map<AbstractShape*, AbstractShapeView*>
. Se você preferir criar as visualizações conforme necessário, faça o seguinte:De qualquer forma, o código fora dessa lógica de criação não precisará lidar com formas concretas. Como parte da destruição de uma forma, é necessário remover a fábrica, obviamente. Obviamente, este exemplo é simplificado demais, mas espero que a idéia seja clara.
Escolhendo a opção certa
Em aplicações muito simples, você pode achar que uma solução suja (fundição) oferece o melhor retorno possível.
A manutenção explícita de listas separadas para cada tipo de concreto é provavelmente o caminho a percorrer se o seu aplicativo lida principalmente com formas de concreto, mas tem algumas partes que são universais. Aqui, faz sentido abstrair apenas na medida em que a funcionalidade comum exigir.
Em geral, vale a pena percorrer todo o caminho se você tiver muita lógica que opera em formas, e o tipo exato de forma realmente é um detalhe do seu aplicativo.
fonte
[rect, rectView]() { rectView.bind(rect); return rectView; }
. A propósito, é claro que isso deve ser feito no módulo de apresentação, por exemplo, em um RectangleCreatedEventHandler.Uma abordagem seria tornar as coisas mais gerais , a fim de evitar a transmissão para tipos específicos .
Você pode implementar um getter / setter básico de propriedades flutuantes de " dimensão " na classe base, que define um valor em um mapa, com base em uma chave específica para o nome da propriedade. Exemplo abaixo:
Então, na sua classe de gerente, você precisa implementar apenas uma função, como abaixo:
Exemplo de uso na Visualização:
Outra sugestão:
Como o seu gerente expõe apenas o levantador e o cálculo do perímetro (que também são expostos pelo Shape), você pode simplesmente instanciar uma Visualização adequada ao instanciar uma classe Shape específica. POR EXEMPLO:
fonte