类Square应该公开继承自Rectangle类吗?

时间:2013-09-19 07:31:42

标签: c++ inheritance public

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

void makeBigger(Rectangle& r) { 

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

} 

此代码对于Rectangle完全没问题,但如果我们将Square对象传递给makeBigger,则会破坏setWidth()对象 - 它的边会变得不相等。


那我怎么处理这个呢?这本书没有提供答案(但是?),但我想到了几种解决这个问题的方法:

  1. 覆盖setHeight()类中的SquareSquare方法,以调整另一方。

    缺点:代码重复,Square不必要的2个成员。

  2. 要让Rectangle不继承自size并独立 - 请setSize()Rectangle等。

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

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

    void makeBigger(NotSquare& r);

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


  4. 有更好的方法吗?我倾向于第三种选择。

6 个答案:

答案 0 :(得分:9)

这是OO设计中的一个关键原则,我发现它处理不正确。迈耶先生非常出色地讨论了你所指的那本书。

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

如果要单独设置矩形的宽度和高度,则不是,方形不是矩形(在软件的上下文中),尽管它是数学上的。因此,您必须考虑将对基础对象执行的操作。

在你提到的具体例子中,有一个规范的答案。如果使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可能是另一个好主意。

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

  4. 我希望有所帮助。请记住,继承只是可以重用代码的一种方法。良好的设计源于所有方法的最佳组合。

    为了进一步阅读,我强烈推荐Sutter和Alexandrescu的“C ++编码标准”,特别是关于类设计和继承的部分。第34项“更喜欢继承权的构成”和37“公共继承权是可替代性的。继承,不是重复使用,而是可以重复使用。

答案 1 :(得分: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 makeBigger(Rectangle const& r)
{
    return r.setWidth(r.width() + 10); 
}

答案 2 :(得分:0)

如果您希望Square 成为 Rectangle,则应该公开继承它。但是,这意味着任何使用Rectangle的公共方法都必须适合Square。在这种情况下

void makeBigger(Rectangle& r)

不应该是一个独立的功能,而是Rectangle中的Square虚拟成员会被using makeBigger覆盖(通过提供自己的)或隐藏(private Rectangle部分)。


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

1使用公共继承并使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()class Rectangle是界面的组成部分,这真的很尴尬而且不合适。在这种情况下,请考虑以下内容。

2您可以拥有一个class Square(可能恰好是方形),也可以选择一个可以转换为static_cast<Rectangle>(square)的{​​{1}} Rectangle 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的{​​{1}}进行更改,则此选项是正确的选择。换句话说,如果您的Square 不是 Rectangle,则在您的代码中实现(具有可独立修改的宽度和高度)。但是,由于Square可以静态转换为Rectangle,因此任何带Rectangle参数的函数也可以使用Square调用。

答案 3 :(得分:0)

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

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

  • 如果你必须介绍更多的类,那么让它变成Shape类,Rectangle,Square和HalfSquare都来自

答案 4 :(得分:0)

你说:“因为每个方格都是一个矩形”,这里问题就在于此。着名的鲍勃马丁的引言:

  

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

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

所以每个正方形都是一个矩形,但这并不意味着表示正方形的类/对象是一个表示矩形的类/对象。

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

答案 5 :(得分:-3)

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

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