在阅读了关于公共继承的“Effective C ++”部分之后,我发现这个问题非常有趣。在我说出是的常识之前,因为每个方格都是矩形,但不一定是其他方式。但请考虑以下代码:
void makeBigger(Rectangle& r) {
r.setWidth(r.width() + 10);
}
此代码对于Rectangle
完全没问题,但如果我们将Square
对象传递给makeBigger
,则会破坏setWidth()
对象 - 它的边会变得不相等。
那我怎么处理这个呢?这本书没有提供答案(但是?),但我想到了几种解决这个问题的方法:
覆盖setHeight()
类中的Square
和Square
方法,以调整另一方。
缺点:代码重复,Square
不必要的2个成员。
要让Rectangle
不继承自size
并独立 - 请setSize()
,Rectangle
等。
缺点:怪异 - 方块毕竟是矩形 - 重用Rectangle
的特征(例如直角等)会很好。
使Rectangle
抽象(通过给它一个纯虚拟析构函数并定义它)并使第三类表示不是正方形的矩形并继承自{{1 }}。这将迫使我们将上述函数的签名更改为:
void makeBigger(NotSquare& r);
除了额外的课程外,看不到任何缺点。
有更好的方法吗?我倾向于第三种选择。
答案 0 :(得分:9)
这是OO设计中的一个关键原则,我发现它处理不正确。迈耶先生非常出色地讨论了你所指的那本书。
诀窍是要记住这些原则必须适用于具体的用例。使用继承时,请记住,当您想将该对象用作时,关键是“是一个”关系适用于对象...因此,方块是否为矩形取决于你将来要用矩形做什么。
如果要单独设置矩形的宽度和高度,则不是,方形不是矩形(在软件的上下文中),尽管它是数学上的。因此,您必须考虑将对基础对象执行的操作。
在你提到的具体例子中,有一个规范的答案。如果使makeBigger成为矩形的虚拟成员函数,则可以以适合类的方式缩放每个函数。但是,如果适用于矩形的所有(公共)方法都适用于正方形,那么这只是一个很好的OO设计。
所以,让我们看看到目前为止这对你的努力有何影响:
我经常在生产代码中看到这种事情。在一个优秀的设计中修复差距是一种可行的方法,但这是不可取的。但这是一个问题,因为它导致代码在语法上是正确的,但在语义上是不正确的。它会编译并执行某些操作,但意义不正确。假设您正在迭代一个矩形向量,并将宽度缩放2,高度缩放3.这对于正方形而言在语义上毫无意义。因此,它违反了“更喜欢编译时错误到运行时错误”的说法。
在这里,您考虑使用继承来重用代码。有一种说法“使用继承来被重用,而不是重用”。这意味着,您希望使用继承来确保oo代码可以在其他地方重新使用,作为其基础对象,而无需任何手动rtti。请记住,还有其他代码重用机制:在C ++中,这些机制包括函数式编程和组合。
如果正方形和矩形具有共享代码(例如,基于它们具有直角的事实来计算区域),则可以通过合成(每个包含公共类)来执行此操作。在这个简单的例子中,你可能最好使用一个函数,例如: compute_area_for_rectangle(Shape * s){return s.GetHeight()* s.GetWidth());} 在命名空间级别提供。
因此,如果Square和Rectangle都继承自基类Shape,Shape具有以下公共方法:draw(),scale(),getArea()...,所有这些对于任何形状都具有语义意义,并且通用公式可以通过命名空间级别函数共享。
我想如果你稍微冥想这一点,你会发现你的第三个建议有很多瑕疵。
关于oo设计观点:正如icbytes所提到的,如果你将要有第三个类,那么这个类是一个有意义地表达常见用途的共同基础更有意义。形状还可以。如果主要目的是绘制对象而不是Drawable可能是另一个好主意。
您表达这个想法的方式还有其他一些缺陷,这可能表明您对虚拟析构函数的误解,以及抽象的意义。每当你创建一个类的方法虚拟,以便另一个类可以覆盖它时,你也应该声明析构函数是虚拟的(S.M。确实在Effective C ++中讨论它,所以我猜你会自己发现它)。这并不是抽象的。当你声明至少一种纯粹虚拟的方法时,它就变得抽象了 - 即没有实现 virtual void foo()= 0; // 例如 这意味着无法实例化有问题的类。显然,因为它至少有一个虚方法,所以它也应该将析构函数声明为虚拟。
我希望有所帮助。请记住,继承只是可以重用代码的一种方法。良好的设计源于所有方法的最佳组合。
为了进一步阅读,我强烈推荐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#那样完全实现,但对我来说这是比第三种选择更好的解决方案。有什么意见吗?