正确的设计可以避免使用dynamic_cast?


9

经过一些研究,我似乎找不到一个简单的例子来解决我经常遇到的问题。

假设我要创建一个小应用程序,可以在其中创建Squares,Circles和其他形状,将它们显示在屏幕上,在选择它们后修改其属性,然后计算其所有周长。

我会像这样做模型类:

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

(想象一下,我有更多种类的形状:三角形,六边形,每次都有它们的proprers变量以及相关的getter和setter。我遇到的问题有8个子类,但是出于示例的原因,我停在了2个位置)

我现在有一个ShapeManager实例化所有形状并将其存储在数组中:

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

最后,我有一个带有旋转框的视图,可以更改每种形状的每个参数。例如,当我在屏幕上选择一个正方形时,参数小部件仅显示与- Square相关的参数(感谢AbstractShape::getType()),并建议更改正方形的宽度。为此,我需要一个函数让我修改中的宽度ShapeManager,这就是我的方法:

void ShapeManager::changeSquareWidth(int shapeIndex, float width){
   Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
   assert(square);
   square->setWidth(width);
}

有没有更好的设计可以避免我对可能拥有的每个子类变量使用dynamic_cast和来实现getter / setter对ShapeManager?我已经尝试使用模板,但是失败了


我现在面临的问题是不是真的与形状,但具有不同的Job小号的3D打印机(例如:PrintPatternInZoneJobTakePhotoOfZone,等)AbstractJob作为其基类。虚方法execute()不是getPerimeter()我唯一需要使用具体用法的方法是填写工作所需的特定信息

  • PrintPatternInZone 需要打印点列表,区域位置以及一些打印参数,例如温度

  • TakePhotoOfZone 需要拍摄照片的区域,保存照片的路径,尺寸等...

当我打电话给时execute(),乔布斯将使用他们所必须的特定信息来实现他们应该采取的行动。

我唯一需要使用作业的具体类型的时间是当我填写或显示这些信息时(如果TakePhotoOfZone Job选择a,将显示一个小部件,用于显示和修改区域,路径和尺寸参数)。

Jobs的再投入的列表Job,其采取的第一个作业S,执行它(通过调用AbstractJob::execute()),则进入下一个,上和,直到列表的末尾。(这就是为什么我使用继承)。

为了存储不同类型的参数,我使用了JsonObject

  • 优点:任何作业的结构相同,设置或读取参数时无需dynamic_cast

  • 问题:无法存储指针(指向PatternZone

您是否有更好的存储数据方式?

那么当我必须修改该类型的特定参数时,如何存储该类型Job的具体类型呢?JobManager只有一个清单AbstractJob*


5
您的ShapeManager似乎将成为God类,因为它基本上将包含所有类型的形状的所有setter方法。
艾默生·卡多佐

您是否考虑过“置物袋”设计?例如,changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)在哪里PropertyKey可以是枚举或字符串,而“ Width”(表示对setter的调用将更新width的值)是允许的值之一。
rwong

即使某些人将财产袋视为OO反模式,但在某些情况下使用财产袋会简化设计,而其他所有选择都会使事情变得更复杂。但是,要确定属性包是否适合您的用例,需要更多信息(例如GUI代码如何与吸气剂/设置剂相互作用)。
rwong

我考虑了属性包设计(即使我不知道它的名称),但是带有JSON对象容器。它肯定可以工作,但我认为这不是一个优雅的设计,可能存在更好的选择。为什么将其视为OO反模式?
11

例如,如果我想存储一个指针以便以后使用,该怎么办?
11

Answers:


10

我想进一步介绍艾默生·卡多佐(Emerson Cardoso)的“其他建议”,因为我相信这在一般情况下是正确的方法-尽管您当然可能会找到其他更适合任何特定问题的解决方案。

问题

在您的示例中,AbstractShape该类具有一种getType()基本上标识具体类型的方法。通常,这表明您没有很好的抽象。毕竟,抽象的全部要点是不必关心具体类型的细节。

另外,如果您不熟悉它,则应该阅读开放/封闭原则。通常会用形状示例进行说明,因此您会感到宾至如归。

有用的抽象

我认为您已经介绍了,AbstractShape因为您发现它对某些事情很有用。很可能,应用程序的某些部分需要知道形状的周长,而不管形状是什么。

这是抽象有意义的地方。由于此模块本身与混凝土形状无关,因此只能依赖于此AbstractShape。出于同样的原因,它不需要该getType()方法-因此您应该摆脱它。

应用程序的其他部分将仅适用于特定的形状,例如Rectangle。这些区域将不会从AbstractShape课程中受益,因此您不应在那里使用它。为了仅将正确的形状传递给这些零件,您需要单独存储混凝土形状。(您可以将它们AbstractShape额外存储,也可以即时组合)。

减少混凝土用量

没有办法解决:在某些地方需要混凝土类型-至少在施工过程中。但是,有时最好将具体类型的使用限制在一些明确定义的区域。这些单独的区域的唯一目的是处理不同的类型-同时将所有应用程序逻辑都排除在外。

您如何实现的?通常,通过引入更多抽象-可能会或可能不会镜像现有的抽象。例如,您的GUI 确实不需要知道它正在处理哪种形状。只需知道屏幕上有一个用户可以编辑形状的区域即可。

所以,你定义一个抽象ShapeEditView您拥有RectangleEditViewCircleEditView持有的宽度/高度或半径的实际文本框的实现。

第一步,您可以在创建RectangleEditView时创建一个Rectangle,然后将其放入std::map<AbstractShape*, AbstractShapeView*>。如果您希望根据需要创建视图,则可以执行以下操作:

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

无论哪种方式,此创建逻辑之外的代码都不必处理具体的形状。显然,作为破坏形状的一部分,您需要删除工厂。当然,此示例过于简化,但我希望这个主意很清楚。

选择正确的选项

在非常简单的应用程序中,您可能会发现肮脏的(广播)解决方案仅能为您带来最大的收益。

如果您的应用程序主要处理混凝土形状,但其中某些部分是通用的,则明确地为每种混凝土类型维护单独的列表是可行的方法。在这里,仅在通用功能需要时才抽象才有意义。

如果您有很多可以在形状上运行的逻辑,那么一路走来通常是值得的,并且形状的确切种类确实是您的应用程序的一个细节。


我非常喜欢您的回答,您完美地描述了问题。我面临的问题不是Shapes,而是3D打印机使用不同的Jobs(例如:PrintPatternInZoneJob,TakePhotoOfZone等),并将AbstractJob作为其基类。虚方法是execute()而不是getPerimeter()。我唯一需要使用的具体用法是用特定的小部件填充作业需要的特定信息(点,位置,温度等的列表)。在这种特殊情况下,为每项工作附加一个观点似乎不是要做的事情,但是我看不出如何使您的视野适应我的铅。
11

如果你不希望保留单独的列表,你可以使用一个视图选择,而不是一个的ViewFactory: [rect, rectView]() { rectView.bind(rect); return rectView; }。顺便说一下,这当然应该在表示模块中完成,例如在RectangleCreatedEventHandler中。
doubleYou

3
话虽如此,请尽量不要过度设计。抽象的好处仍然必须超过附加管道的成本。有时,放置适当的转换或单独的逻辑可能更可取。
doubleYou

2

一种方法是使内容更通用,以避免转换为特定类型

您可以在基类中实现基本的getter / setter 维度属性float,它基于属性名称的特定键在映射中设置值。下面的例子:

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

然后,在经理类中,您只需实现一个功能,如下所示:

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

视图中的用法示例:

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

另一个建议:

由于您的经理仅公开设置者和周长计算(这也由Shape公开),因此您可以在实例化特定Shape类时简单地实例化适当的View。例如:

  • 实例化一个Square和一个SquareEditView;
  • 将Square实例传递给SquareEditView对象;
  • (可选)在主视图中,您可以保留Shape列表,而不是使用ShapeManager。
  • 在SquareEditView中,您保留对Square的引用;这样就无需进行转换以编辑对象。

我喜欢第一个建议,并且已经考虑过,但是如果要存储不同的变量(浮点数,指针,数组),这是非常有限的。对于第二个建议,如果正方形已经实例化(我在视图上点赞了它),我怎么知道它是一个Square *对象?存储形状的列表将返回AbstractShape *
11

@ElevenJune-是的,所有建议都有其缺点;首先,如果需要更多类型的属性,则需要实现更复杂的功能,而不是简单的映射。第二个建议改变了形状的存储方式。您可以将基本形状存储在列表中,但同时您需要提供特定形状对视图的引用。也许您可以提供有关您的方案的更多详细信息,所以我们可以评估这些方法是否比仅执行dynamic_cast更好。
艾默生卡多佐

@ElevenJune-拥有视图对象的全部目的是让您的GUI不必知道它正在与Square类型的类一起使用。view对象提供了“查看”该对象(无论您定义的是什么)所必需的内容,并且在内部它知道它正在使用Square类的实例。GUI仅与SquareView实例交互。因此,您无法单击“方形”课程。您只能单击SquareView类。更改SquareView上的参数将更新基础Square类。...–
Dunk

...这种方法很可能使您摆脱ShapeManager类。几乎可以肯定,这将简化您的设计。我总是说,如果您将某个班级称为“经理”,那么您会认为这是一个糟糕的设计,并想出了其他办法。经理类之所以坏,有多种原因,最明显的是神类问题,而且没人知道该类的实际作用,可以做和不能做的事实,因为经理甚至可以做与他们管理的事情相关的任何事情。您可以打赌,跟随您的那些开发人员将利用这种优势,获得典型的大泥巴。
Dunk

1
...您已经遇到了这个问题。到底为什么要让经理成为改变形状尺寸的经理才有意义?经理为什么要计算形状的周长?如果您不知道,我喜欢“另一个建议”。
Dunk
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.