Après avoir fait quelques recherches, je n'arrive pas à trouver un exemple simple pour résoudre un problème que je rencontre souvent.
Disons que je veux créer une petite application où je peux créer des Square
s, Circle
s et d'autres formes, les afficher sur un écran, modifier leurs propriétés après les avoir sélectionnées, puis calculer tous leurs périmètres.
Je ferais la classe modèle comme ceci:
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;
};
(Imaginez que j'ai plus de classes de formes: triangles, hexagones, avec à chaque fois leurs variables proprers et getters et setters associés. Les problèmes que j'ai rencontrés avaient 8 sous-classes, mais pour l'exemple, je me suis arrêté à 2)
J'ai maintenant un ShapeManager
, instanciant et stockant toutes les formes dans un tableau:
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;
};
Enfin, j'ai une vue avec des spinbox pour changer chaque paramètre pour chaque type de forme. Par exemple, lorsque je sélectionne un carré à l'écran, le widget de paramètres affiche uniquement les Square
paramètres liés (grâce à AbstractShape::getType()
) et propose de changer la largeur du carré. Pour ce faire, j'ai besoin d'une fonction me permettant de modifier la largeur en ShapeManager
, et voici comment je le fais:
void ShapeManager::changeSquareWidth(int shapeIndex, float width){
Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
assert(square);
square->setWidth(width);
}
Existe-t-il une meilleure conception m'évitant d'utiliser le dynamic_cast
et d'implémenter un couple getter / setter ShapeManager
pour chaque variable de sous-classe que je pourrais avoir? J'ai déjà essayé d'utiliser le modèle mais j'ai échoué .
Le problème que je suis confronté est pas vraiment avec des formes mais avec différentes Job
s pour une imprimante 3D (ex: PrintPatternInZoneJob
, TakePhotoOfZone
, etc.) avec AbstractJob
comme leur classe de base. La méthode virtuelle est execute()
et non getPerimeter()
. La seule fois où j'ai besoin d'utiliser concrètement est de remplir les informations spécifiques dont un travail a besoin :
PrintPatternInZone
a besoin de la liste des points à imprimer, de la position de la zone, de certains paramètres d'impression comme la températureTakePhotoOfZone
a besoin de quelle zone à prendre en photo, le chemin où la photo sera enregistrée, les dimensions, etc ...
Lorsque j'appellerai ensuite execute()
, les Jobs utiliseront les informations spécifiques dont ils disposent pour réaliser l'action qu'ils sont censés faire.
La seule fois où j'ai besoin d'utiliser le type concret d'un Job est quand je remplis ou affiche ces informations (si a TakePhotoOfZone
Job
est sélectionné, un widget affichant et modifiant les paramètres de zone, de chemin et de dimensions sera affiché).
Les Job
s sont ensuite mis dans une liste de Job
s qui prennent le premier job, l'exécute (en appelant AbstractJob::execute()
), le passe au suivant, et ainsi de suite jusqu'à la fin de la liste. (C'est pourquoi j'utilise l'héritage).
Pour stocker les différents types de paramètres, j'utilise JsonObject
:
avantages: même structure pour n'importe quel travail, pas de dynamic_cast lors du réglage ou de la lecture des paramètres
problème: impossible de stocker des pointeurs (vers
Pattern
ouZone
)
Pensez-vous qu'il existe une meilleure façon de stocker les données?
Alors, comment stockeriez-vous le type concret duJob
pour l'utiliser lorsque je dois modifier les paramètres spécifiques de ce type? JobManager
n'a qu'une liste de AbstractJob*
.
la source
changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)
oùPropertyKey
peut être une énumération ou une chaîne, et "Largeur" (ce qui signifie que l'appel au setter mettra à jour la valeur de largeur) est parmi l'une des valeurs autorisées.Réponses:
Je voudrais approfondir «l'autre suggestion» d'Emerson Cardoso, car je pense que c'est la bonne approche dans le cas général - bien que vous puissiez bien sûr trouver d'autres solutions mieux adaptées à un problème particulier.
Le problème
Dans votre exemple, la
AbstractShape
classe a unegetType()
méthode qui identifie essentiellement le type concret. C'est généralement un signe que vous n'avez pas une bonne abstraction. Après tout, l'abstrait n'a pas à se soucier des détails du type concret.De plus, si vous ne le connaissez pas, vous devriez lire sur le principe ouvert / fermé. Il est souvent expliqué avec un exemple de formes, donc vous vous sentirez comme chez vous.
Abstractions utiles
Je suppose que vous avez introduit le
AbstractShape
car vous l'avez trouvé utile pour quelque chose. Très probablement, une partie de votre application doit connaître le périmètre des formes, quelle que soit la forme.C'est l'endroit où l'abstraction prend tout son sens. Parce que ce module ne se préoccupe pas des formes concrètes, il ne peut dépendre
AbstractShape
que de lui. Pour la même raison, il n'a pas besoin de lagetType()
méthode - vous devez donc vous en débarrasser.D'autres parties de l'application ne fonctionneront qu'avec un type particulier de forme, par exemple
Rectangle
. Ces zones ne bénéficieront pas d'uneAbstractShape
classe, vous ne devriez donc pas l'utiliser là-bas. Afin de ne transmettre que la forme correcte à ces pièces, vous devez stocker les formes en béton séparément. (Vous pouvez les stocker enAbstractShape
plus ou les combiner à la volée).Minimiser l'utilisation du béton
Il n'y a aucun moyen de contourner cela: vous avez besoin des types de béton à certains endroits - tout au moins pendant la construction. Cependant, il est parfois préférable de limiter l'utilisation de types de béton à quelques zones bien définies. Ces zones distinctes ont pour seul but de traiter les différents types - tandis que toute logique d'application est gardée à l'écart.
Comment y parvenez-vous? Habituellement, en introduisant plus d'abstractions - qui peuvent ou non refléter les abstractions existantes. Par exemple, votre interface graphique n'a pas vraiment besoin de savoir de quel type de forme il s'agit. Il suffit de savoir qu'il y a une zone sur l'écran où l'utilisateur peut modifier une forme.
Vous définissez donc un résumé
ShapeEditView
pour lequel vous disposezRectangleEditView
et desCircleEditView
implémentations qui contiennent les zones de texte réelles pour la largeur / hauteur ou le rayon.Dans un premier temps, vous pouvez créer un
RectangleEditView
chaque fois que vous créez unRectangle
, puis le placer dans unstd::map<AbstractShape*, AbstractShapeView*>
. Si vous préférez créer les vues selon vos besoins, vous pouvez effectuer les opérations suivantes à la place:De toute façon, le code en dehors de cette logique de création n'aura pas à traiter de formes concrètes. Dans le cadre de la destruction d'une forme, vous devez évidemment supprimer l'usine. Bien sûr, cet exemple est trop simplifié, mais j'espère que l'idée est claire.
Choisir la bonne option
Dans des applications très simples, vous trouverez peut-être qu'une solution sale (coulée) vous donne le plus pour votre argent.
La gestion explicite de listes distinctes pour chaque type de béton est probablement la voie à suivre si votre application traite principalement de formes en béton, mais comporte des parties universelles. Ici, il est logique de n'abstraire que dans la mesure où la fonctionnalité commune l'exige.
Aller jusqu'au bout est généralement payant si vous avez beaucoup de logique qui opère sur les formes, et le type exact de forme est vraiment un détail pour votre application.
la source
[rect, rectView]() { rectView.bind(rect); return rectView; }
. Soit dit en passant, cela devrait bien sûr être fait dans le module de présentation, par exemple dans un RectangleCreatedEventHandler.Une approche consisterait à rendre les choses plus générales afin d' éviter de les transtyper en types spécifiques .
Vous pouvez implémenter un getter / setter de base de propriétés flottantes " dimension " dans la classe de base, qui définit une valeur dans une carte, basée sur une clé spécifique pour le nom de la propriété. Exemple ci-dessous:
Ensuite, dans votre classe de gestionnaire, vous devez implémenter une seule fonction, comme ci-dessous:
Exemple d'utilisation dans la vue:
Une autre suggestion:
Étant donné que votre gestionnaire expose uniquement le setter et le calcul de périmètre (qui sont également exposés par Shape), vous pouvez simplement instancier une vue appropriée lorsque vous instanciez une classe Shape spécifique. PAR EXEMPLE:
la source