3

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

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

到目前为止很容易实现。但是这些矩形应该能够实现不同的功能。这可能是序列化器、样式器、抽屉等...

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

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 ++中做这样的事情?

4

3 回答 3

3

首先,

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

多重继承不是“不行”

(否则它将被排除在语言之外)。多重继承是应该小心使用的东西。你应该明白,你在做什么,在使用它的时候。

在您的情况下,您似乎不太可能遇到钻石问题,这是多重继承的邪恶。

递归模板模式允许您在编译时检查启用的功能,就像这样

#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 会使代码更加复杂,并且必须在所有头文件中实现。

加起来,

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

于 2013-06-01T21:24:14.510 回答
3

在我提出不同的解决方案之前,让我指出您解决此问题的方法的一些缺点,每种解决方案都有其优点和缺点。

多重继承方法的缺点

  • Stackable类/接口继承将聚合数据结构的知识构建到表示数据的类中(Rect)。Rect在s 需要进入多个数据结构(例如,用于查找的树)或数据结构也需要包含s 的情况下,这可能会受到限制Circle。这种解决方案也使得以后很难换出数据结构。
  • 拥有大量不同的类组合可能会导致无法管理的类数量必须随着时间的推移进行维护。
  • 如果其他人需要处理您的代码,但没有注意到您已经定义了 type StackableStyleableDrawableRect。然后他或她继续定义他们自己的DrawableStackableStyleableRect,提供相同的功能但与您的类不同。在最好的情况下,您的项目中现在有冗余代码。更糟糕的是,当您需要混合使用这两个类时,您会遇到问题和混乱,因为已经存在使用这两个类的部分代码库。
  • 一旦在程序中引入了另一个问题,例如调整Rects 的大小,我们是更改所有现有类还是创建更多新类?我们是更改StackableStyleableDrawableRectStackableStyleableDrawableResizeableRect,提示对现有代码库进行更改,还是将其创建为一个全新的类?
  • 当然,对于多重继承,如果你不小心,或者如果有一天你决定 a既是 a又是 a并且需要调用类似的东西,你就会冒着引入菱形问题的风险。RectGDIDrawableDirectXDrawableDrawable::logDrawingOperation()

Rect因此,虽然is-a Drawable、Stackable 等看起来微不足道,但这种方法很麻烦并且有很多缺点。我相信在这种情况下,Rect除了普通的矩形之外没有任何业务,并且不应该知道项目的任何其他子系统。

可能的替代解决方案

我能想到两种替代解决方案,但每一种都在可读性与灵活性以及编译时复杂性与运行时复杂性之间进行通常的权衡。

混音

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

访客模式

访问者模式允许我们将遍历对象层次结构与对其进行操作的算法分开。由于每个对象都知道自己的类型,因此即使不知道该算法是什么,它也可以调度正确的算法。为了说明使用Shape,CircleRect

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. 这样我们就可以遍历并绘制整个堆栈。

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

于 2013-06-01T21:50:41.763 回答
2

不知道我是否正确理解了您的问题,但基于策略的类设计可能值得一看:http ://en.wikipedia.org/wiki/Policy-based_design 。它最初是在 Alexandrescu 的《现代 C++ 设计》一书中介绍的。它允许通过从所谓的策略派生类来扩展类。

机制看起来像:

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

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

希望我能帮上忙

于 2013-06-01T21:39:29.030 回答