从多个组件构建c ++类

时间:2013-06-01 19:41:53

标签: c++ inheritance multiple-inheritance crtp

我正在开发一个系统,我的矩形可以按层次顺序堆叠。所以基数是这样的:

Rect parent;
Rect child;
parent.addChild(&child);
...
Rect* someChild = parent.getChildAt(1);

到目前为止易于实施。但是这些矩形应该能够实现不同的功能。这可能是Serializer,Styler,Drawer等......

现在我知道多重继承是一种“不行”但在这种情况下我会发现这样的语法:

class StackableStyleableRect: publicRect, public Stackable, Styleable{}
class StackableStyleableDrawableRect: public Rect, public Stackable, Styleable, Drawable{}

我偶然发现了奇怪的重复出现的模板模式(crtp),如果我理解正确,它将使上述成为可能。像这样:

class Rect{
    public:
       float width;
       float height;
}

template <class RectType>
class Stackable{
    public:
        void addChild(RectType* c){
             children.push_back(c);
        }
        std::vector<RectType*> children;
}

template <class RectType>
class Drawable{
    public:
        virtual void draw(){
            RectType* r static_cast<RectType>(this);
            drawRect(r->width, r->height);
        }
}

template <class RectType>
class Styleable{
    public:
        int r, g, b;
}

class CustomRect: public Rect, public Stackable<CustomRect>, public Drawable<CustomRect>{
}

class CustomRectWithStyle: public Rect, public Stackable<CustomRect>, public Drawable<CustomRect>, public Styleable<CustomRect>{
    public:
}

这样做的原因是我想重用不同类型的Rect类型的代码。在一个项目中,我不需要设置样式,而在另一个项目中,我需要提供所有功能。通过这种方式来选择所需的功能,它可以保持干净并且还可以分离功能。

我用这个进行了一些基本测试,它按预期工作,但我觉得语法可能会随着时间的推移变得过于复杂。

同样在某些时候,如果组件在附近,使组件彼此依赖或使它们的行为不同将是有用的。 (例如,Drawable的绘图功能可以自动使用Styleable中的颜色(如果存在)

现在我注定迟早会遇到麻烦或者可能会有用吗?是否有更适合的模式?或者根本不可能在“正确的”c ++中做这样的事情?

3 个答案:

答案 0 :(得分:3)

首先,

我不假装是额外的C ++专家。

多重继承不是“不去”

(否则将被排除在语言之外)。 Multiplle继承就是这个,应该小心使用 。在使用它时你应该明白,你在做什么。

在您的情况下,您遇到diamond problem似乎不太可能,这是多重继承的罪恶。

递归模板模式允许您在编译时中检查已启用的功能,如下所示

#define FEATURED(FEATURE, VALUE) \
template <template<class>class Feature = FEATURE> \
typename std::enable_if<std::is_base_of<Feature<RectType>, RectType>::value == VALUE>::type

template <class RectType>
class Styleable;

template <class RectType>
class Drawable{
    public:

        FEATURED(Styleable, true)
        drawImpl()
        {
            std::cout << "styleable impl\n";
        }

        FEATURED(Styleable, false)
        drawImpl()
        {
            std::cout << "not styleable impl\n";
        }


        virtual void draw(){
            drawImpl();
        }
};

您可以在具有正常继承的功能中实现类似的功能,但似乎无法进行编译时功能检查。

另一方面,您使用crtp会使代码更复杂,并且您必须在所有头文件中实现。

总结,

我认为这很复杂,你应该确定你确实需要它。它将工作一段时间,直到它将被重新设计,与任何其他代码一样。它的寿命主要取决于你的任务理解。

答案 1 :(得分:3)

在我提出不同的解决方案之前,让我指出一下你解决这个问题的方法的一些缺点,每种解决方案各有其优缺点。

多重继承方法的缺点

  • 继承Stackable类/接口,将聚合数据结构的知识构建到表示数据(Rect)的类中。在Rect需要进入多个数据结构(例如,查找树)或数据结构中也需要Circle的情况下,这可能会受到限制。此解决方案还使以后更换数据结构变得困难。
  • 有许多不同的班级组合可能会导致无法管理的课程数量,必须随着时间的推移而维持。
  • 其他人是否需要处理您的代码,但没有注意到您已定义类型StackableStyleableDrawableRect。然后,他或她继续定义自己的DrawableStackableStyleableRect,它提供相同的功能,但与您的班级不同。在最好的情况下,您现在在项目中有冗余代码。更糟糕的是,当你需要混合使用这两个类时,你会遇到问题和混乱,因为代码库的部分已经存在,使用它们。
  • 一旦在程序中引入了另一个问题,例如调整Rect的大小,我们是否会更改所有现有类或创建更多新类?我们是否将StackableStyleableDrawableRect更改为StackableStyleableDrawableResizeableRect,提示更改现有代码库,还是将其创建为全新的类?
  • 当然,对于多重继承,如果您不小心,或者有一天您认为Rect都是GDIDrawable,则可能会引入diamond problem。和DirectXDrawable并需要调用类似Drawable::logDrawingOperation()的内容。

因此虽然Rect是可绘制的,可堆叠的等似乎微不足道,但这种方法很麻烦并且有许多缺点。我相信在这种情况下,Rect除了普通的矩形之外没有任何业务,不应该了解项目的任何其他子系统。

可能的替代解决方案

我可以想到两种备用解决方案,但每种解决方案通常都会考虑可读性与灵活性以及编译时复杂性与运行时复杂性之间的关系。

混合插件

如图here所示,通过模板巧妙使用混合可以避免MI方法的一些问题,尽管不是全部。最重要的是,它创建了一个奇怪的继承层次结构并增加了编译时的复杂性。一旦我们向Shape层次结构中添加了更多类,它也会分解,因为Drawable只知道如何绘制Rect

访客模式

visitor pattern允许我们将对象层次结构与对其进行操作的算法分开。由于每个对象都知道自己的类型,因此即使不知道算法是什么,它也可以调度正确的算法。要说明使用ShapeCircleRect

class Shape
{
    public:
        virtual void accept(class Visitor &v) = 0;
};

class Rect : public Shape
{
   public:
       float width;
       float height;

       void accept(class Visitor &v)
       {
           v.visit(this);
       }
};

class Circle : public Shape
{
    public:
        float radius;

       void accept(class Visitor &v)
       {
           v.visit(this);
       }
};

class Visitor
{
    public:
        virtual void visit(Rect *e) = 0;
        virtual void visit(Circle *e) = 0;
};

class ShapePainter : public Visitor
{
    // Provide graphics-related implementations for the two methods.
};

class ShapeSerializer : public Visitor
{
    // Provide methods to serialize each shape.
};

通过这样做,我们牺牲了一些运行时复杂性,但是将我们的各种关注点与我们的数据分离开来。现在可以轻松地在程序中添加新的关注点。我们需要做的就是添加另一个Visitor类来完成我们想要的操作,并将Shape::accept()与这个新类的对象结合使用,如下所示:

class ShapeResizer : public Visitor
{
    // Resize Rect.
    // Resize Circle.
};

Shape *shapey = new Circle();
ShapeResizer sr;
shapey->accept(sr);

这种设计模式还有一个优点,即如果你忘记实现某些数据/算法组合但在程序中使用它,编译器会抱怨。我们可能希望稍后覆盖Shape::accept()以定义聚合形状类型,例如ShapeStack。这样我们就可以遍历并绘制整个堆栈。

我认为如果性能在您的项目中不是至关重要的,那么访问者解决方案就更胜一筹。如果您需要满足实时约束条件,也可能值得考虑,但它不会使程序速度变慢到足以危及最后期限。

答案 2 :(得分:2)

不知道我是否理解你的问题,但也许基于政策的课程设计可能值得一看:http://en.wikipedia.org/wiki/Policy-based_design。它首先在Alexandrescu的书 Modern C ++ Design 中引入。它允许通过从所谓的策略中派生类来扩展类。

机制看起来像:

template<class DrawingPolicy> class Rectangle : public DrawingPolicy { ... };

其中DrawingPolicy提供了一个draw(void)方法,例如,它在Rectangle类中可用。

希望我能帮忙