4

在阅读了关于公共继承的“Effective C++”部分后,我发现这个问题非常有趣。在我说是的常识之前,因为每个正方形都是矩形,但不一定是其他方式。但是请考虑以下代码:

void makeBigger(Rectangle& r) { 

    r.setWidth(r.width() + 10); 

} 

这段代码对于 a 来说非常好,但是如果我们将它传递给它Rectangle会破坏对象- 它的两边会变得不相等。SquaremakeBigger


那么我该如何处理呢? 这本书没有提供答案(还没有?),但我正在考虑解决这个问题的几种方法:

  1. 类中的重写setWidth()setHeight()方法Square也可以调整另一边。

    缺点:代码重复,不必要的Square.

  2. 为了Square不继承Rectangle和独立 - 拥有sizesetSize()

    缺点:很奇怪 - 正方形毕竟是矩形 - 重用Rectangle's 的特征(例如直角等)会很好。

  3. 抽象化(通过给Rectangle它一个纯虚析构函数并定义它)并有第三个类表示不是正方形的矩形并继承自Rectangle. 这将迫使我们将上述函数的签名更改为:

    void makeBigger(NotSquare& r);

    除了有一个额外的课程之外,看不到任何缺点。


有没有更好的办法?我倾向于第三种选择。

4

6 回答 6

10

这是我发现处理不当的 OO 设计中的关键原则之一。迈耶先生在讨论你所指的书方面做得非常出色。

诀窍是记住这些原则必须应用于具体的用例。使用继承时,请记住,关键是当您想将该对象用作对象时,“is a”关系适用于该对象......因此,正方形是否为矩形取决于您将要做什么未来使用矩形。

如果您要独立设置矩形的宽度和高度,那么不,正方形不是矩形(在您的软件的上下文中),尽管它是数学上的。因此,您必须考虑您将如何处理您的基础对象。

在您提到的具体示例中,有一个规范的答案。如果你让 makeBigger 成为矩形的虚拟成员函数,那么每个都可以以适合类的方式缩放。但是,如果适用于矩形的所有(公共)方法都适用于正方形,那么这只是一个好的 OO 设计。

因此,让我们看看这如何适用于您迄今为止的努力:

  1. 我经常在生产代码中看到这种事情。在其他方面很好的设计中弥补差距是可以原谅的,但这是不可取的。但这是一个问题,因为它导致代码在语法上是正确的,但在语义上是不正确的。它会编译,并做一些事情,但意思是不正确的。假设您正在迭代一个矩形向量,并且您将宽度缩放 2,高度缩放 ​​3。这对于正方形来说在语义上没有意义。因此,它违反了“宁愿编译时错误胜过运行时错误”的原则。

  2. 在这里,您正在考虑使用继承来重用代码。有句话叫“用继承来重用,而不是用来重用”。这意味着,您希望使用继承来确保 oo 代码可以在其他地方重用,作为其基础对象,而无需任何手动 rtti。请记住,还有其他代码重用机制:在 C++ 中,这些机制包括函数式编程和组合。

    如果正方形和矩形具有共享代码(例如,根据它们具有直角的事实计算面积),您可以通过组合来做到这一点(每个都包含一个公共类)。在这个简单的示例中,您可能最好使用一个函数,例如:在命名空间级别提供的 compute_area_for_rectangle(Shape* s){return s.GetHeight() * s.GetWidth());}。

    因此,如果 Square 和 Rectangle 都继承自基类 Shape,Shape 具有以下公共方法:draw()、scale()、getArea() ...,所有这些对于任何形状都将在语义上有意义,并且通用公式可以通过命名空间级函数共享。

  3. 我想如果你稍微思考一下这一点,你会发现你的第三个建议有很多缺陷。

    关于 oo 设计观点:正如 icbytes 所提到的,如果你要拥有第三个类,那么这个类是一个有意义地表达常见用途的公共基础更有意义。形状还可以。如果主要目的是绘制对象,那么 Drawable 可能是另一个好主意。

    您表达想法的方式还有其他一些缺陷,这可能表明您对虚拟析构函数以及抽象的含义存在误解。每当您将类的方法设为虚拟以便另一个类可以覆盖它时,您也应该将析构函数声明为虚拟(SM 确实在 Effective C++ 中讨论了这一点,所以我想您会自己发现这一点)。这并没有使它抽象。当您声明至少一种纯虚拟方法时,它就变得抽象了——即没有实现
    virtual void foo() = 0; // 例如这意味着有问题的类不能被实例化。显然,因为它至少有一个虚拟方法,它也应该有声明为虚拟的析构函数。

我希望这会有所帮助。请记住,继承只是可以重用代码的一种方法。好的设计来自所有方法的最佳组合。

为了进一步阅读,我强烈推荐 Sutter 和 Alexandrescu 的“C++ 编码标准”,尤其是关于类设计和继承的部分。第 34 条“优先组合而不是继承”和第 37 条“公共继承是可替代性。继承,不是重用,而是被重用。

于 2013-09-19T08:29:49.730 回答
5

事实证明,更简单的解决方案是

Rectangle makeBigger(Rectangle r)
{
    r.setWidth(r.width() + 10); 
    return r;
}

在正方形上效果很好,即使在这种情况下也能正确返回一个矩形。

[编辑]评论指出,真正的问题是对setWidth. 这可以用相同的方式修复:

Rectangle Rectangle::setWidth(int newWidth) const
{
  Rectangle r(*this);
  r.m_width = newWidth;
  return r;
}

同样,改变正方形的宽度会给你一个矩形。如图const所示,它为您提供了一个新Rectangle的,而无需更改现有的 Rectangle 以前的功能现在变得更加容易:

Rectangle makeBigger(Rectangle const& r)
{
    return r.setWidth(r.width() + 10); 
}
于 2013-09-19T08:34:58.310 回答
0

如果你希望你Square 成为一个 Rectangle,它应该公开继承它。但是,这意味着任何与 a 一起使用的公共方法都Rectangle必须针对 a 进行适当的专门化Square。在这种情况下

void makeBigger(Rectangle& r)

不应该是一个独立的函数,而是一个虚拟成员,Rectangle它的 inSquare被覆盖(通过提供它自己的)或隐藏(通过 using makeBiggerprivate部分中)。


关于一些你可以做的事情不能对aRectangle做的问题Square。这是一个普遍的设计困境,C++ 与设计无关。如果有人对 a 的引用(或指针)Rectangle实际上是 aSquare并且想要执行对 a 没有意义的操作Square,那么您必须处理它。有几种选择:

1 使用公共继承并在Square尝试操作时抛出异常Square

struct Rectangle {
  double width,height;
  virtual void re_scale(double factor)
  { width*=factor; height*=factor; }
  virtual void change_width(double new_width)       // makes no sense for a square
  { width=new_width; }
  virtual void change_height(double new_height)     // makes no sense for a square
  { height=new_height; }
};

struct Square : Rectangle {
  double side;
  void re_scale(double factor)
  { side *= factor; }                               // fine
  void change_width(double)
  { throw std::logic_error("cannot change width for Sqaure"); }
  virtual void change_height(double)
  { throw std::logic_error("cannot change height for Sqaure"); }
};

change_width()如果或者change_height()是界面的组成部分,这确实很尴尬并且不合适。在这种情况下,请考虑以下事项。

2 你可以有一个class Rectangle(可能碰巧是方形的),也可以有一个单独class Square的,可以转换为 ( static_cast<Rectangle>(square)) 为 aRectangle并因此充当矩形,但不能像 a 那样修改Rectangle

struct Rectangle {
  double width,height;
  bool is_square() const
  { return width==height; }
  Rectangle(double w, double h) : width(w), height(h) {}
};

// if you still want a separate class, you can have it but it's not a Rectangle 
// though it can be made convertible to one
struct Square {
  double size;
  Square(Rectangle r) : size(r.width)   // you may not want this throwing constructor
  { assert(r.is_square()); }
  operator Rectangle() const            // conversion to Rectangle
  { return Rectangle(size,size); }
};

如果您允许更改Rectangle可以将其变为Square. 换句话说,如果您Square 不是 代码中实现的 ,Rectangle(具有可独立修改的宽度和高度)。但是,由于可以静态转换为 a ,因此也可以使用 a 调用任何带参数的函数。SquareRectangleRectangleSquare

于 2013-09-19T07:47:02.303 回答
0

除了有一个额外的课程之外,您的第三种解决方案(也称为Factor out modifiers)没有严重的缺点。我能想到的只有:

  • 假设我有一个派生的 Rectangle 类,其中一条边是另一条边的一半,例如 HalfSquare。然后根据您的第三个解决方案,我必须再定义一个类,称为 NotHalfSaquare。

  • 如果你必须介绍更多的类,那就让它成为形状类,矩形、正方形和半正方形都派生自

于 2013-09-19T07:53:50.897 回答
0

您说:“因为每个正方形都是矩形”,问题就在这里。著名鲍勃·马丁名言的释义:

对象之间的关系不由它们的代表共享。

(原文解释:http: //blog.bignerdranch.com/1674-what-is-the-liskov-substitution-principle/

所以肯定每个正方形都是矩形,但这并不意味着代表正方形的类/对象“是”代表矩形的类/对象。

现实世界中最常见的、不那么抽象和直观的例子是:如果两名律师在离婚的情况下代表丈夫和妻子在法庭上发生争执,那么尽管律师在离婚期间代表人民并且目前已婚,但他们自己没有结婚,也没有离婚。

于 2013-09-19T15:32:21.523 回答
-3

我的想法:你有一个超类,叫做 Shape。Square 继承自 Shape。它有方法 resize(int size)。一个Rectangle就是ClassRectangle,继承自Shape,但是实现了IRecangle接口。IRectangle 有方法 resize_rect(int sizex, int size y)。

在 C++ 中,接口是通过使用所谓的纯虚方法创建的。它没有像在 c# 中那样完全实现,但对我来说,这比第三个选项更好。有什么意见吗?

于 2013-09-19T07:46:41.313 回答