我应该如何设计一组相关的分类,其中只有一些分类支持特定的操作

How should I design a set of related classed where only some of them support a certain operation?

本文关键字:分类 操作 支持 一组 我应该      更新时间:2023-10-16

我正在用C++开发一个基于幻灯片的应用程序。每个幻灯片都有一个幻灯片项目集合,可以包括标题、按钮、矩形等项目。

只有其中一些项目支持填充,而其他项目则支持不要。

在这种情况下,实现幻灯片项目填充的最佳方式是什么?以下是我想到的两种方式:

  1. 创建接口Fillable并为幻灯片项目实现此接口支持填充,将所有与填充相关的属性保留在接口中。迭代幻灯片项目列表时,dynamic_cast进入Fillable,如果成功,则执行与填充相关的操作。

  2. 制作一个fill类。将fill指针作为幻灯片项目类的一部分,分配对于那些支持填充的对象,fill对象指向fill指针,而对于其余对象,则将其保持为空。给出一个函数GetFill,如果它存在,它将返回项目的fill,否则返回NULL

最好的方法是什么?我对性能和可维护性感兴趣。

我会将两者结合起来。制作Fillable接口,并将其作为GetFill方法的返回类型。这比动态铸造方法要好。使用动态转换查询接口需要实际的幻灯片项对象实现接口(如果要支持它的话(。但是,使用像GetFill这样的访问器方法,您可以选择提供指向实现接口的其他对象的引用/指针。如果接口实际上是由这个对象实现的,那么您也可以只返回this。这种灵活性可以帮助避免类膨胀,并促进创建可由多个类共享的可重用组件对象。

编辑:这种方法也适用于null对象模式。您可以返回一个实现接口的简单无操作对象,而不是为不支持Fillable的对象返回一个空指针。这样就不必担心总是检查客户端代码中的空指针。

答案是这取决于

如果不是每个对象都应该处理填充,那么我不认为必须用fill/get_fillable_instance/...扰乱基本界面有什么意义。然而,你只需要就可以逃脱惩罚

struct slide_object
{
    virtual void fill() {} // default is to do nothing
};

但这取决于您是否认为CCD_ 16应该出现在幻灯片对象抽象类中。然而,它很少应该这样做,除非不可填充是例外。

如果您需要提供两个不同的对象类(并且不超过两个(,其中一些是可填充的,另一个与可填充性无关,那么动态强制转换是正确的。在这种情况下,有两个子层次结构并在需要的地方使用动态强制转换是有意义的。

在某些情况下,我已经成功地使用了这种方法,并且它简单且可维护,前提是调度逻辑不分散(即只有一两个地方可以进行动态转换(。

如果你期望有更多类似填充的行为,那么dynamic_cast是一个错误的选择,因为它会导致

if (auto* p = dynamic_cast<fillable*>(x))
   ...
else if (auto* p = dynamic_cast<quxable*>(x))
   ...

这很糟糕。如果您需要这个,那么实现一个Visitor模式。

创建基类SlideItem:

class SlideItem {
    public:
        virtual ~SlideItem();
        virtual void fill() = 0;
};

然后为那些你不能填充的东西做一个空的实现:

class Button : public SlideItem {
    public:
        void fill() { }
};

以及其他人的适当填充实施:

class Rectangle : public SlideItem {
    public:
        void fill() { /* ... fill stuff ... */ }
};

把它们都放在一个容器里。。如果你想填满他们,就给每个人打电话。。。易于维护。。谁关心性能:(


如果您真的需要快速代码,那么您的第一个解决方案当然很好。但如果你这样做,确保你不必每次都想填充它。将它们投射一次,然后将指针放入可填充的容器中。如果必须填充,则对这个可填充容器进行迭代。

再说一遍,IMHO你在这方面付出了太多的努力,却没有获得合理的性能提升。(当然,我不知道你的应用程序,它可能是合理的……但通常不是(

你想要的似乎接近能力模式。你的#2接近这个模式。我会这么做:

制作填充类。将填充指针作为幻灯片项类的一部分,将填充对象分配给仅支持填充的对象的填充指针,其余对象保持为空。创建一个函数GetCapability(Capability.Fill(,如果存在,它将返回项的填充,否则返回NULL。如果某些对象已经实现了可填充接口,则可以将对象强制转换返回到可填充指针。

考虑存储变量项,如boost::variant

你可以定义一个boost::variant<Fillable*,Item*>(如果你有所有权,你应该使用智能指针(,然后有一个要迭代的变体列表。

我建议使用一个用于形状的接口,并使用一个返回填充符的方法。例如:

class IFiller {
public:
    virtual void Fill() = 0;
protected:
    IFiller() {}
    virtual ~IFiller() {}
};
class IShape {
public:
    virtual IFiller* GetFiller() = 0;
protected:
    IShape() {}
    virtual ~IShape() {}
};
class NullFiller : public IFiller {
public:
    void Fill() { /* Do nothing */ }
};
class Text : public IShape {
public:
    IFiller* GetFiller() { return new NullFiller(); }
};
class Rectangle;
class RectangleFiller : public IFiller {
public:
    RectangleFiller(Rectangle* rectangle) { _rectangle = rectangle; }
    ~RectangleFiller() {}
    void Fill() { /* Fill rectangle space */ }
private:
    Rectangle* _rectangle;
};
class Rectangle : IShape {
public:
    IFiller* GetFiller() { return new RectangleFiller(this); }
};

我发现这种方法更容易维护和扩展,同时不会引入主要的性能问题。