it-swarm.com.de

Richtiges Design, um die Verwendung von dynamic_cast zu vermeiden?

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, in der ich Square, Circle und andere Formen erstellen, sie auf einem Bildschirm anzeigen, ihre Eigenschaften nach Auswahl ändern und dann alle berechnen kann ihrer Perimeter.

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, mit jedem 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 in ShapeManager ändern kann. 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 davon abhält, dynamic_cast Zu verwenden und ein Getter/Setter-Paar in ShapeManager für jede Unterklassenvariable zu implementieren, die ich möglicherweise habe? Ich habe bereits versucht, Vorlage zu verwenden, aber fehlgeschlagen .


Das Problem, mit dem ich konfrontiert bin, ist nicht wirklich bei Shapes, sondern bei verschiedenen Job s für einen 3D-Drucker (Beispiel: PrintPatternInZoneJob, TakePhotoOfZone usw.) mit AbstractJob als 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 und einige Druckparameter wie die Temperatur

  • TakePhotoOfZone 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 execute() aufrufe, 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 ein TakePhotoOfZoneJob ausgewählt ist, wird ein Widget angezeigt, das die Parameter für Zone, Pfad und Dimensionen anzeigt und ändert.

Die Job werden dann in eine Liste von Job gesetzt, die den ersten Job annehmen, ihn ausführen (durch Aufrufen von AbstractJob::execute()), der zum nächsten übergeht, on und bis zum Ende der Liste. (Deshalb verwende ich Vererbung).

Um die verschiedenen Arten von Parametern zu speichern verwende ich ein JsonObject:

  • vorteile: gleiche Struktur für jeden Job, kein dynamic_cast beim Setzen oder Lesen von Parametern

  • problem: Zeiger können nicht gespeichert werden (auf Pattern oder Zone)

Gibt es Ihrer Meinung nach eine bessere Möglichkeit, Daten zu speichern?

Wie würden Sie dann den konkreten Typ des Job speichern, um ihn zu verwenden, wenn ich die spezifischen Parameter dieses Typs ändern muss? JobManager hat nur eine Liste von AbstractJob*.

9
ElevenJune

Ich möchte auf Emerson Cardosos "anderen Vorschlag" eingehen, weil 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 Klasse AbstractShape über eine Methode getType(), 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 gehe davon aus, dass Sie das AbstractShape eingeführt haben, 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 nur von AbstractShape abhängen. Aus dem gleichen Grund wird die Methode getType() nicht benötigt - Sie sollten sie daher entfernen.

Andere Teile der Anwendung funktionieren nur mit einer bestimmten Art von Form, z. Rectangle. Diese Bereiche profitieren nicht von einer AbstractShape -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 sie zusätzlich als AbstractShape 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.

Sie definieren also ein abstraktes ShapeEditView, für das Sie RectangleEditView und CircleEditView Implementierungen haben, die die tatsächlichen Textfelder für Breite/Höhe oder Radius enthalten.

In einem ersten Schritt können Sie ein RectangleEditView erstellen, wenn Sie ein Rectangle erstellen und es dann in ein std::map<AbstractShape*, AbstractShapeView*>. Wenn Sie die Ansichten lieber nach Bedarf erstellen möchten, können Sie stattdessen Folgendes tun:

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

In beiden Fällen muss sich der Code außerhalb dieser Erstellungslogik nicht mit konkreten Formen befassen. Im Rahmen 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 sich Ihre Anwendung hauptsächlich mit Betonformen befasst, jedoch 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.

10
doubleYou

Ein Ansatz wäre, Dinge allgemeiner zu machen um das Casting auf bestimmte Typen zu vermeiden.

Sie können einen grundlegenden Getter/Setter mit den Float-Eigenschaften " dimension " in der Basisklasse implementieren, der einen Wert in einer Map basierend auf einem bestimmten Schlüssel festlegt für den Eigenschaftsnamen. Beispiel unten:

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

Dann müssen Sie in Ihrer Manager-Klasse nur eine Funktion implementieren, wie unten:

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

Anwendungsbeispiel in der Ansicht:

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

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:

  • Instanziieren Sie ein Square und ein SquareEditView.
  • Übergeben Sie die Square-Instanz an das SquareEditView-Objekt.
  • (optional) Anstatt einen ShapeManager zu haben, können Sie in Ihrer Hauptansicht immer noch eine Liste der Formen führen.
  • In SquareEditView behalten Sie einen Verweis auf ein Quadrat bei. Dies würde das Casting zum Bearbeiten der Objekte überflüssig machen.
2
Emerson Cardoso