Nach einigen Recherchen kann ich anscheinend kein einfaches Beispiel finden, um ein Problem zu lösen, auf das ich häufig stoße.
Angenommen, ich möchte eine kleine Anwendung erstellen Square
, in der ich s, Circle
s und andere Formen erstellen , sie auf einem Bildschirm anzeigen, ihre Eigenschaften nach Auswahl ändern und dann alle ihre Umfänge berechnen kann.
Ich würde die Modellklasse so machen:
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;
};
(Stellen Sie sich vor, ich habe mehr Klassen von Formen: Dreiecke, Sechsecke, jedes Mal ihre Proprers-Variablen und zugehörige Getter und Setter. Die Probleme, mit denen ich konfrontiert war, hatten 8 Unterklassen, aber für das Beispiel habe ich bei 2 angehalten.)
Ich habe jetzt eine ShapeManager
, die alle Formen in einem Array instanziiert und speichert:
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;
};
Schließlich habe ich eine Ansicht mit Spinboxen, um jeden Parameter für jeden Formtyp zu ändern. Wenn ich beispielsweise ein Quadrat auf dem Bildschirm auswähle, zeigt das Parameter-Widget nur Square
-bezogene Parameter an (dank AbstractShape::getType()
) und schlägt vor, die Breite des Quadrats zu ändern. Dazu benötige ich eine Funktion, mit der ich die Breite ändern kann ShapeManager
, und so mache ich das:
void ShapeManager::changeSquareWidth(int shapeIndex, float width){
Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
assert(square);
square->setWidth(width);
}
Gibt es ein besseres Design, das mich daran hindert, das dynamic_cast
Getter / Setter-Paar ShapeManager
für jede Unterklassenvariable zu verwenden und zu implementieren ? Ich habe bereits versucht, eine Vorlage zu verwenden, bin jedoch fehlgeschlagen .
Das Problem , das ich bin Bewurf ist nicht wirklich mit Formen , aber mit unterschiedlichem Job
s für einen 3D - Drucker (zB: PrintPatternInZoneJob
, TakePhotoOfZone
, etc.) mit AbstractJob
als ihre Basisklasse. Die virtuelle Methode ist execute()
und nicht getPerimeter()
. Das einzige Mal, dass ich konkrete Verwendung verwenden muss, ist das Ausfüllen der spezifischen Informationen, die ein Job benötigt :
PrintPatternInZone
benötigt die Liste der zu druckenden Punkte, die Position der Zone, einige Druckparameter wie die TemperaturTakePhotoOfZone
benötigt die Zone, in die das Foto aufgenommen werden soll, den Pfad, in dem das Foto gespeichert wird, die Abmessungen usw.
Wenn ich dann anrufe execute()
, verwenden die Jobs die spezifischen Informationen, die sie benötigen, um die Aktion zu realisieren, die sie ausführen sollen.
Das einzige Mal, dass ich den konkreten Typ eines Jobs verwenden muss, ist, wenn ich diese Informationen ausfülle oder anzeige (wenn a TakePhotoOfZone
Job
ausgewählt ist, wird ein Widget angezeigt, das die Parameter für Zone, Pfad und Dimensionen anzeigt und ändert).
Die Job
s werden dann in eine Liste von Job
s eingefügt, die den ersten Job annehmen, ihn ausführen (durch Aufrufen AbstractJob::execute()
), die zum nächsten weitergehen, weiter und weiter bis zum Ende der Liste. (Deshalb verwende ich Vererbung).
Um die verschiedenen Arten von Parametern zu speichern, verwende ich a JsonObject
:
Vorteile: gleiche Struktur für jeden Job, kein dynamic_cast beim Setzen oder Lesen von Parametern
Problem: Zeiger (auf
Pattern
oderZone
) können nicht gespeichert werden
Gibt es Ihrer Meinung nach eine bessere Möglichkeit, Daten zu speichern?
Dann , wie würden Sie die konkrete Art der SpeicherungJob
, es zu benutzen , wenn ich die spezifischen Parameter dieses Typs ändern müssen? JobManager
hat nur eine Liste von AbstractJob*
.
quelle
changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)
woPropertyKey
kann eine Aufzählung oder eine Zeichenfolge sein, und "Breite" (was bedeutet, dass der Aufruf des Setters den Wert der Breite aktualisiert) gehört zu den zulässigen Werten.Antworten:
Ich möchte auf Emerson Cardosos "anderen Vorschlag" eingehen, da ich glaube, dass dies im allgemeinen Fall der richtige Ansatz ist - obwohl Sie natürlich andere Lösungen finden können, die für ein bestimmtes Problem besser geeignet sind.
Das Problem
In Ihrem Beispiel verfügt die
AbstractShape
Klasse über einegetType()
Methode, die im Grunde den konkreten Typ identifiziert. Dies ist im Allgemeinen ein Zeichen dafür, dass Sie keine gute Abstraktion haben. Der springende Punkt beim Abstrahieren ist schließlich, dass man sich nicht um die Details des konkreten Typs kümmern muss.Falls Sie damit nicht vertraut sind, sollten Sie sich über das Open / Closed-Prinzip informieren. Es wird oft anhand eines Formbeispiels erklärt, damit Sie sich wie zu Hause fühlen.
Nützliche Abstraktionen
Ich nehme an, Sie haben das eingeführt,
AbstractShape
weil Sie es für etwas nützlich fanden. Höchstwahrscheinlich muss ein Teil Ihrer Anwendung den Umfang der Formen kennen, unabhängig von der Form.Dies ist der Ort, an dem Abstraktion Sinn macht. Da sich dieses Modul nicht mit konkreten Formen befasst, kann es
AbstractShape
nur davon abhängen . Aus dem gleichen Grund wird diegetType()
Methode nicht benötigt - Sie sollten sie also entfernen.Andere Teile der Anwendung funktionieren nur mit einer bestimmten Form, z
Rectangle
. Diese Bereiche profitieren nicht von einerAbstractShape
Klasse, daher sollten Sie sie dort nicht verwenden. Um diesen Teilen nur die richtige Form zu geben, müssen Sie Betonformen separat speichern. (Sie können sieAbstractShape
zusätzlich speichern oder im laufenden Betrieb kombinieren).Minimierung des Betonverbrauchs
Daran führt kein Weg vorbei: An einigen Stellen benötigen Sie die Betontypen - zumindest während des Baus. Manchmal ist es jedoch am besten, die Verwendung von Betontypen auf einige genau definierte Bereiche zu beschränken. Diese getrennten Bereiche haben ausschließlich den Zweck, sich mit den verschiedenen Typen zu befassen - während die gesamte Anwendungslogik aus ihnen herausgehalten wird.
Wie erreichen Sie das? In der Regel durch Einführung weiterer Abstraktionen, die die vorhandenen Abstraktionen widerspiegeln können oder nicht. Zum Beispiel muss Ihre GUI nicht wirklich wissen, um welche Art von Form es sich handelt. Es muss nur bekannt sein, dass es auf dem Bildschirm einen Bereich gibt, in dem der Benutzer eine Form bearbeiten kann.
So können Sie eine Zusammenfassung definieren ,
ShapeEditView
für die Sie habenRectangleEditView
undCircleEditView
Implementierungen, die die eigentlichen Textfelder für Breite / Höhe oder Radius halten.In einem ersten Schritt können Sie ein erstellen,
RectangleEditView
wann immer Sie ein erstellen,Rectangle
und es dann in ein einfügenstd::map<AbstractShape*, AbstractShapeView*>
. Wenn Sie die Ansichten lieber nach Bedarf erstellen möchten, können Sie stattdessen Folgendes tun:In beiden Fällen muss sich der Code außerhalb dieser Erstellungslogik nicht mit konkreten Formen befassen. Als Teil der Zerstörung einer Form müssen Sie natürlich die Fabrik entfernen. Natürlich ist dieses Beispiel zu stark vereinfacht, aber ich hoffe, die Idee ist klar.
Auswahl der richtigen Option
In sehr einfachen Anwendungen stellen Sie möglicherweise fest, dass eine schmutzige (Gieß-) Lösung Ihnen nur das Beste für Ihr Geld gibt.
Das explizite Verwalten separater Listen für jeden Betontyp ist wahrscheinlich der richtige Weg, wenn Ihre Anwendung sich hauptsächlich mit konkreten Formen befasst, aber einige Teile enthält, die universell sind. Hier ist es sinnvoll, nur insoweit zu abstrahieren, als die gemeinsame Funktionalität dies erfordert.
Es lohnt sich im Allgemeinen, den ganzen Weg zu gehen, wenn Sie eine Menge Logik haben, die mit Formen arbeitet, und die genaue Art der Form ist wirklich ein Detail für Ihre Anwendung.
quelle
[rect, rectView]() { rectView.bind(rect); return rectView; }
. Dies sollte übrigens natürlich im Präsentationsmodul erfolgen, zB in einem RectangleCreatedEventHandler.Ein Ansatz wäre, Dinge allgemeiner zu gestalten, um zu vermeiden, dass bestimmte Typen gegossen werden .
Sie können in der Basisklasse einen grundlegenden Getter / Setter für Float-Eigenschaften " dimension " implementieren , der einen Wert in einer Map basierend auf einem bestimmten Schlüssel für den Eigenschaftsnamen festlegt. Beispiel unten:
Dann müssen Sie in Ihrer Manager-Klasse nur eine Funktion implementieren, wie unten:
Anwendungsbeispiel in der Ansicht:
Ein weiterer Vorschlag:
Da Ihr Manager nur den Setter und die Perimeterberechnung (die auch von Shape verfügbar gemacht werden) verfügbar macht, können Sie einfach eine richtige Ansicht instanziieren, wenn Sie eine bestimmte Formklasse instanziieren. Z.B:
quelle