4

好的,我在 C++ 中有一个有点复杂的系统。简而言之,我需要向第三方抽象基类添加一个方法。第三方还提供了大量也需要新功能的派生类。

我正在使用一个提供标准 Shape 接口以及一些常见形状的库。

class Shape
{
    public:
        Shape(position);
        virtual ~Shape();

        virtual position GetPosition() const;
        virtual void SetPosition(position);

        virtual double GetPerimeter() const = 0;

    private: ...
};

class Square : public Shape
{
    public:
        Square(position, side_length);
    ...
};

class Circle, Rectangle, Hexagon, etc

现在,这是我的问题。我希望 Shape 类也包含一个 GetArea() 函数。所以看起来我应该做一个:

class ImprovedShape : public virtual Shape
{
    virtual double GetArea() const = 0;
};

class ImprovedSquare : public Square, public ImprovedShape
{
    ...
}

然后我去做一个继承自ImprovedShape 和Square 的ImprovedSquare。好吧,正如你所看到的,我现在已经创建了可怕的钻石继承问题。如果第三方库对其 Square、Circle 等使用虚拟继承,这将很容易解决。但是,让他们这样做并不是一个合理的选择。

那么,当您需要向库中定义的接口添加一点功能时,您会怎么做?有好的答案吗?

谢谢!

4

10 回答 10

7

为什么这个类需要从形状派生?

class ImprovedShape : public virtual Shape
{
    virtual double GetArea() const = 0;
};

为什么不只是有

class ThingWithArea 
{
    virtual double GetArea() const = 0;
};

ImprovementSquare 是一个 Shape 并且是一个 ThingWithArea

于 2008-10-30T09:33:33.830 回答
4

我想外观模式应该可以解决问题。

将第 3 方接口包装成您自己的接口,并且您的应用程序代码使用包装器接口而不是第 3 方接口。这样,您也可以很好地隔离不受控制的第 3 方界面中的更改。

于 2008-10-30T07:44:36.267 回答
4

我们在一个项目中遇到了一个非常相似的问题,我们只是通过不从 Shape 派生改进形状来解决它。如果您需要在 ImprovementShape 中使用 Shape 功能,您可以使用 dynamic_cast,因为您知道您的演员将始终有效。其余的就像你的例子一样。

于 2008-10-30T07:52:35.580 回答
4

也许您应该阅读正确的继承,并得出结论,ImprovedShape 不需要从 Shape 继承,而是可以将 Shape 用于其绘图功能,类似于第 21.12 点关于 SortedList 如何不必继承的 FAQ 中的讨论from List 即使它想提供相同的功能,它也可以简单地使用List。

以类似的方式,ImprovedShape 可以使用Shape 来完成它的 Shape 事情。

于 2008-10-30T09:48:44.863 回答
2

可能用于装饰器模式?[ http://en.wikipedia.org/wiki/Decorator_pattern][1]

于 2008-10-30T09:58:48.197 回答
1

是否可以采用完全不同的方法——使用模板和元编程技术?如果您不限于不使用模板,这可以提供一个优雅的解决方案。只有ImprovedShapeImprovedSquare改变:

template <typename ShapePolicy>
class ImprovedShape : public ShapePolicy
{
public:
    virtual double GetArea();
    ImprovedShape(void);
    virtual ~ImprovedShape(void);

protected:
    ShapePolicy shape;
    //...
};

ImprovedSquare变成:

class ImprovedSquare : public ImprovedShape<Square>
{
public:
    ImprovedSquare(void);
    ~ImprovedSquare(void);

    // ...

};

您将避免菱形继承,从原始形状(通过策略类)获得继承以及您想要的附加功能。

于 2008-10-30T14:52:12.673 回答
1

Dave Hillier 的方法是正确的。分离GetArea()成自己的界面:

class ThingWithArea
{
public:
    virtual double GetArea() const = 0;
};

如果 Shape 的设计者做了正确的事情并把它变成了一个纯接口,并且具体类的公共接口足够强大,你就可以将具体类的实例作为成员。这就是你如何得到SquareWithArea (ImprovedSquare是一个糟糕的名字) aShape和 a ThingWithArea

class SquareWithArea : public Shape, public ThingWithArea
{
public:
    double GetPerimeter() const { return square.GetPerimeter(); }
    double GetArea() const { /* do stuff with square */ }

private:
    Square square;
};

不幸的是,Shape设计者在 中进行了一些实现Shape,最终您将携带两个副本SquareWithArea,就像您最初提出的钻石一样。

这几乎迫使您进入最紧密耦合,因此也是最不可取的解决方案:

class SquareWithArea : public Square, public ThingWithArea
{
};

如今,从 C++ 中的具体类派生被认为是不好的形式。很难找到一个很好的解释为什么你不应该这样做。通常,人们会引用 Meyers 的《更有效的 C++ 条款 33》,其中指出不可能写出像样operator=()的东西。那么,您可能永远不应该为具有值语义的类这样做。另一个陷阱是具体类没有虚拟析构函数(这就是为什么你永远不应该公开从 STL 容器派生的原因)。在这里都不适用。居高临下送你去C++ faq学习继承的发帖人是错的——加GetArea()不违反 Liskov 可替代性。我能看到的唯一风险来自在具体类中重写虚函数,当实现者后来更改名称并默默地破坏您的代码时。

综上所述,我认为你可以问心无愧地从 Square 衍生出来。(作为安慰,您不必为 Shape 接口编写所有转发函数)。

现在解决需要两个接口的功能问题。我不喜欢不必要dynamic_cast的 s。相反,让函数引用两个接口,并在调用站点为两个接口传递对同一对象的引用:

void PrintPerimeterAndArea(const Shape& s, const ThingWithArea& a)
{
    cout << s.GetPerimeter() << endl;
    cout << a.GetArea() << endl;
}

// ...

SquareWithArea swa;
PrintPerimeterAndArea(swa, swa);

所有PrintPerimeterAndArea()需要做的工作都是周长和面积的来源。它不关心这些碰巧在同一个对象实例上实现为成员函数。可以想象,该区域可以由它和Shape.

这使我们遇到了唯一一种我会考虑传入一个引用并通过另一个引用获得另一个的情况dynamic_cast——这两个引用指向同一个对象实例很重要。这是一个非常人为的例子:

void hardcopy(const Shape& s, const ThingWithArea& a)
{
    Printer p;
    if (p.HasEnoughInk(a.GetArea()))
    {
        s.print(p);
    }
}

即使那样,我可能更愿意发送两个参考而不是 dynamic_cast. 我将依靠一个健全的整体系统设计来消除将两个不同实例的位提供给这样的功能的可能性。

于 2008-11-02T11:21:38.430 回答
1

另一种关于元编程/mixin 的尝试,这一次有点受特征的影响。它假定计算面积是您要根据暴露的属性添加的东西;你可以做一些保持封装的事情,这是一个目标,而不是模块化。但是你必须为每个子类型编写一个 GetArea,而不是尽可能使用多态的。这是否值得取决于您对封装的承诺程度,以及您的库中是否有可以利用常见行为的基类,例如下面的RectangularShape

#import <iostream>

using namespace std;

// base types
class Shape {
    public:
        Shape () {}
        virtual ~Shape () { }
        virtual void DoShapyStuff () const = 0;
};

class RectangularShape : public Shape {
    public:
        RectangularShape () { }

        virtual double GetHeight () const = 0 ;
        virtual double GetWidth  () const = 0 ;
};

class Square : public RectangularShape {
    public:
        Square () { }

        virtual void DoShapyStuff () const
        {
            cout << "I\'m a square." << endl;
        }

        virtual double GetHeight () const { return 10.0; }
        virtual double GetWidth  () const { return 10.0; }
};

class Rect : public RectangularShape {
    public:
        Rect () { }

        virtual void DoShapyStuff () const
        {
            cout << "I\'m a rectangle." << endl;
        }

        virtual double GetHeight () const { return 9.0; }
        virtual double GetWidth  () const { return 16.0; }
};

// extension has a cast to Shape rather than extending Shape
class HasArea {
    public:
        virtual double GetArea () const = 0;
        virtual Shape& AsShape () = 0;
        virtual const Shape& AsShape () const = 0;

        operator Shape& ()
        {
            return AsShape();
        }

        operator const Shape& () const
        {
            return AsShape();
        }
};

template<class S> struct AreaOf { };

// you have to have the declaration before the ShapeWithArea 
// template if you want to use polymorphic behaviour, which 
// is a bit clunky
static double GetArea (const RectangularShape& shape)
{
    return shape.GetWidth() * shape.GetHeight();
}

template <class S>
class ShapeWithArea : public S, public HasArea {
    public:
        virtual double GetArea () const
        {
            return ::GetArea(*this);
        }
        virtual Shape& AsShape ()             { return *this; }
        virtual const Shape& AsShape () const { return *this; }
};

// don't have to write two implementations of GetArea
// as we use the GetArea for the super type
typedef ShapeWithArea<Square> ImprovedSquare;
typedef ShapeWithArea<Rect> ImprovedRect;

void Demo (const HasArea& hasArea)
{
    const Shape& shape(hasArea);
    shape.DoShapyStuff();
    cout << "Area = " << hasArea.GetArea() << endl;
}

int main ()
{
    ImprovedSquare square;
    ImprovedRect   rect;

    Demo(square);
    Demo(rect);

    return 0;
}
于 2008-11-02T12:47:49.707 回答
1

GetArea() 不必是成员。它可以是模板函数,以便您可以为任何形状调用它。

就像是:

template <class ShapeType, class AreaFunctor> 
int GetArea(const ShapeType& shape, AreaFunctor func);

STL minmax函数可以被认为是您的案例的类比。给定比较器函数,您可以找到对象数组/向量的最小值和最大值。同样,只要提供计算面积的函数,您就可以得出任何给定形状的面积。

于 2010-09-29T16:40:27.410 回答
0

正如我所理解的那样,您的问题存在解决方案。使用addapter-pattern。适配器模式用于向特定类添加功能或交换特定行为(即方法)。考虑到您绘制的场景:

class ShapeWithArea : public Shape
{
 protected:
  Shape* shape_;

 public:
  virtual ~ShapeWithArea();

  virtual position GetPosition() const { return shape_->GetPosition(); }
  virtual void SetPosition(position)   { shape_->SetPosition(); }
  virtual double GetPerimeter() const  { return shape_->GetPerimeter(); }

  ShapeWithArea (Shape* shape) : shape_(shape) {}

  virtual double getArea (void) const = 0;
};

适配器模式旨在适应类的行为或功能。你可以用它来

  • 通过不转发而是重新实现方法来改变类的行为。
  • 通过添加方法向类添加行为。

它如何改变行为?当你为方法提供一个 base 类型的对象时,你也可以提供适配的类。对象将按照您的指示行事,对象上的参与者只关心基类的接口。您可以将此适配器应用于 Shape 的任何派生项。

于 2008-11-19T12:21:38.763 回答