如果我们重写SetWidth和SetHeight方法,为什么Square从Rectangle继承会出现问题?


105

如果Square是Rectangle的一种类型,那么为什么Square无法从Rectangle继承?还是为什么设计不好?

我听说有人说:

如果您使Square从Rectangle派生,那么Square应该可以在您期望矩形的任何地方使用

这里有什么问题?为何Square可以在您期望矩形的任何地方使用?仅当我们创建Square对象,并且重写Square的SetWidth和SetHeight方法时,它才可用,为什么会有任何问题呢?

如果您在Rectangle基类上具有SetWidth和SetHeight方法,并且Rectangle引用指向一个Square,则SetWidth和SetHeight毫无意义,因为设置一个将更改另一个以匹配它。在这种情况下,Square无法通过带有矩形的Liskov替代测试,并且从Square继承Square的抽象是一个不好的选择。

有人可以解释以上论点吗?同样,如果我们在Square中重写SetWidth和SetHeight方法,它是否可以解决此问题?

我也听说过:

真正的问题是我们不是在建模矩形,而是在“舒适的矩​​形”上建模,即在创建后可以修改宽度或高度的矩形(并且我们仍然将其视为同一对象)。如果我们以这种方式看待矩形类,很明显,正方形不是“可接受的矩形”,因为正方形无法重塑,并且仍然是正方形(通常)。在数学上,我们看不到问题,因为在数学环境中可变性甚至都没有意义

在这里,我认为“可调整大小”是正确的术语。矩形是“可调整大小的”,正方形也是如此。我在上述论点中缺少什么吗?可以像调整任何矩形一样调整正方形的大小。


15
这个问题似乎非常抽象。使用类和继承有无数种方法,无论是否使某个类从某个类继承都是一个好主意,通常取决于您要如何使用这些类。没有实际案例,我将看不到该问题如何获得相关答案。
2014年

2
使用一些常识可以回想起正方形一个矩形,因此,如果不能在需要矩形的地方使用正方形类的对象,那么无论如何它可能是一些应用程序设计上的缺陷。
克苏鲁2014年

7
我认为更好的问题是Why do we even need Square?就像有两支钢笔一样。一支蓝色的笔和一支红色的蓝色,黄色或绿色的笔。蓝色笔是多余的-在正方形的情况下更是如此,因为它没有成本优势。
Gusdor 2014年

2
@eBusiness它的抽象性使它成为一个很好的学习问题。能够独立于特定用例而识别出子类型的哪些使用不好是很重要的。
2014年

5
@Cthulhu不是。子类型化是关于行为的,可变的正方形的行为不像可变的矩形。这就是为什么“是...”的隐喻不好的原因。
2014年

Answers:


189

基本上,我们希望事情表现得明智。

考虑以下问题:

我得到了一组矩形,我想将其面积增加10%。所以我要做的是将矩形的长度设置为以前的1.1倍。

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

现在,在这种情况下,我所有矩形的长度都增加了10%,这将使它们的面积增加了10%。不幸的是,实际上有人通过了正方形和矩形的混合体,并且当矩形的长度改变时,宽度也改变了。

我的单元测试通过了,因为我编写了所有单元测试以使用矩形的集合。现在,我在我的应用程序中引入了一个细微的错误,这个错误可能在几个月后才被忽略。

更糟糕的是,来自会计的Jim看到了我的方法,并编写了一些其他代码,该代码利用以下事实:如果他将平方数传递到我的方法中,那么他的大小就会增加21%。吉姆很高兴,没有人比他更明智。

吉姆因出色的工作而晋升到另一个部门。阿尔弗雷德(Alfred)以初级职位加入公司。Advertising的Jill在他的第一份bug报告中报告说,将方格传递给该方法会导致21%的增长,并且希望修复该bug。Alfred看到Square和Rectangles在代码中随处可见,并且意识到打破继承链是不可能的。他也无权访问Accounting的源代码。因此,Alfred修复了如下错误:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

阿尔弗雷德(Alfred)对他的超级黑客技术感到满意,而吉尔(Jill)则表示该错误已修复。

下个月没有人得到报酬,因为会计依赖于能够对IncreaseRectangleSizeByTenPercent方法进行平方并增加21%的面积。整个公司都进入“优先级1错误修正”模式,以跟踪问题的根源。他们将问题追溯到阿尔弗雷德(Alfred)的解决方法。他们知道必须使会计和广告两全其美。因此,他们通过使用方法调用识别用户来解决问题,如下所示:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

等等等等。

这个轶事是基于每天面对程序员的现实情况。违反Liskov Substitution原则可能会引入非常细微的错误,这些错误只有在编写多年后才会被发现,到那时修复此违规将破坏一堆事情,而未修复它会激怒您的最大客户。

有两种解决此问题的现实方法。

第一种方法是使Rectangle不可变。如果Rectangle的用户无法更改Length和Width属性,则此问题将消失。如果要使用不同长度和宽度的矩形,可以创建一个新的矩形。正方形可以愉快地从矩形继承。

第二种方法是打破正方形和矩形之间的继承链。如果将正方形定义为具有单个SideLength属性,而将矩形定义为具有LengthWidth属性并且没有继承,则不可能通过期望矩形并获得正方形来意外破坏事物。用C#术语来说,您可以使用seal矩形类,以确保您获得的所有矩形实际上都是矩形。

在这种情况下,我喜欢解决问题的“不可变对象”方法。矩形的标识是矩形的长度和宽度。有意义的是,当您要更改对象的标识时,您真正想要的是一个对象。如果您失去了一个老客户并获得了一个新客户,则无需将Customer.Id字段从老客户更改为新客户,而是创建一个新客户Customer

违反Liskov替换原理在现实世界中很常见,这主要是因为那里的许多代码是由不称职/在时间压力下/不在乎/犯错误的人编写的。它会并且确实会导致一些非常令人讨厌的问题。在大多数情况下,您宁愿使用组合而不是继承


7
Liskov是一回事,而存储又是另一回事。在大多数实现中,从Rectangle继承的Square实例将需要空间来存储二维,即使只需要一个维。
el.pescado 2014年

29
巧妙地利用故事来说明要点
罗里·亨特

29
好故事,但我不同意。用例是:更改矩形的面积。该修复程序应在专门用于Square的矩形中添加一个可覆盖的方法'ChangeArea'。这不会破坏继承链,明确用户想要做什么,也不会引起您提到的修复程序引入的错误(可能会在适当的暂存区域中捕获该错误)。
Roy T. 2014年

33
@RoyT .:为什么矩形应该知道如何设置其面积?这是一个完全从长度和宽度派生的属性。更重要的是,它应该更改哪个尺寸-长度,宽度或两者?
cHao 2014年

32
@Roy T.非常高兴地说您已经以不同的方式解决了该问题,但是事实是,尽管简化了,但这是开发人员在维护旧产品时每天面对的现实情况的一个示例(尽管简化了)。即使您实现了该方法,也不会阻止继承者违反LSP并引入类似于该方法的错误。这就是.NET框架中几乎每个类都被密封的原因。
斯蒂芬

30

如果您的所有对象都是不可变的,则没有问题。每个正方形也是矩形。矩形的所有属性也是正方形的属性。

当您添加了修改对象的功能时,问题就开始了。或者说真的-当您开始将参数传递给对象时,不仅要读取属性获取器。

您可以对Rectangle进行一些修改,以保留Rectangle类的所有不变量,但不保留所有Square不变量-例如更改宽度或高度。突然,矩形的行为不仅是其属性,还可能是其修改。这不仅是您从矩形中获得的东西,而且还可以放入其中

如果Rectangle具有setWidth记录为更改宽度而不修改高度的方法,则Square不能具有兼容的方法。如果更改宽度而不是高度,则结果将不再是有效的Square。如果选择在使用时同时修改Square的宽度和高度,则说明setWidth您未实现Rectangle的规范setWidth。你就是赢不了。

当您查看可以“放入”矩形和正方形的内容,可以发送给它们的消息时,您可能会发现可以有效发送到正方形的任何消息,也可以发送给矩形。

这是协方差还是反方差的问题。

适当的子类的方法(一种可以在所有需要超类的情况下使用实例的子类)需要每种方法:

  • 仅返回超类将返回的值-也就是说,返回类型必须是超类方法的返回类型的子类型。收益是协变的。
  • 接受超类型将接受的所有值-也就是说,参数类型必须是超类方法的参数类型的超类型。参数是反变的。

因此,回到Rectangle和Square:Square是否可以作为Rectangle的子类完全取决于Rectangle拥有的方法。

如果Rectangle对于宽度和高度有单独的设置器,Square将不会成为一个好的子类。

同样,如果使某些方法在参数中是协变的,例如compareTo(Rectangle)在Rectangle和compareTo(Square)Square上具有,则将Square作为Rectangle会遇到问题。

如果将Square和Rectangle设计为兼容,则可能会起作用,但应该将它们一起开发,否则我敢打赌,它将无法工作。


“如果所有的对象是不可改变的,没有问题” -这是在这个问题上,其中明确提到了制定者的宽度和高度的情况下显然是不相关的声明
蚊蚋

11
我发现这很有趣,即使它与“显然无关”
Jesvin Jose 2014年

14
@gnat我认为这很重要,因为当两种类型之间存在有效的子类型关系时,该问题的真正价值就在于认识到。这取决于超类型声明的操作,因此值得指出的是,如果mutator方法消失了,问题就消失了。
2014年

1
@gnat也是,setters是mutators,所以lrn本质上是在说:“不要那样做,这不是问题。” 我碰巧同意简单类型的不可变性,但是您有一个很好的观点:对于复杂对象,问题并不那么简单。
Patrick M

1
以这种方式考虑,“矩形”类所保证的行为是什么?您可以彼此更改宽度和高度INDEPENDENT。(即setWidth和setHeight)方法。现在,如果Square是从Rectangle派生的,Square必须保证这种行为。由于square无法保证此行为,因此它是不良继承。但是,如果从Rectangle类中删除了setWidth / setHeight方法,则不会出现这种行为,因此可以从Rectangle派生Square类。
Nitin Bhide 2014年

17

这里有很多好的答案。斯蒂芬的答案尤其能很好地说明为什么违反替代原则会导致团队之间发生现实冲突。

我想我可能会简短地谈论矩形和正方形的特定问题,而不是将其用作其他违反LSP的隐喻。

正方形是一种特殊的矩形还有一个很少提及的问题,那就是:为什么我们以正方形和矩形停下来?如果我们愿意说正方形是一种特殊的矩形,那么我们当然也应该愿意说:

  • 正方形是一种特殊的菱形-它是具有直角的菱形。
  • 菱形是一种特殊的平行四边形-它是具有相等边的平行四边形。
  • 矩形是一种特殊的平行四边形-它是具有直角的平行四边形
  • 矩形,正方形和平行四边形都是一种特殊的梯形-它们是具有两组平行边的梯形
  • 以上所有都是特殊的四边形
  • 以上都是特殊的平面形状
  • 等等; 我可以在这里继续一段时间。

所有关系应该在这里到底是什么?诸如C#或Java之类的基于类继承的语言并非旨在表示具有多种不同类型约束的这类复杂关系。最好通过不尝试将所有这些东西表示为具有子类型关系的类来完全避免该问题。


3
如果形状对象是不可变的,则可以具有IShape包括边界框的类型,并且可以对其进行绘制,缩放和序列化,并且可以具有IPolygon子类型,该子类型具有报告顶点数的方法和返回的方法IEnumerable<Point>。那么人们可以有IQuadrilateral亚型从派生IPolygonIRhombusIRectangle,从中导出,并ISquare派生自IRhombusIRectangle。可变性会把所有东西丢给窗口,并且多重继承不适用于类,但是我认为使用不可变的接口就可以了。
2014年

我在这里实际上不同意埃里克(尽管对于-1来说还不够!)。所有这些关系都是(可能)相关的,如@supercat所提到的;这只是一个YAGNI问题:您需要实现它才能实现它。
马克·赫德

很好的答案!应该更高。
andrew.fox

1
@MarkHurd-这不是YAGNI的问题:建议的基于继承的层次结构的形状类似于所描述的分类法,但是无法编写以保证定义它的关系。如何IRhombus保证PointEnumerable<Point>定义中返回的所有内容都IPolygon与等长的边相对应?因为IRhombus仅接口的实现不能保证具体对象是菱形,所以继承不能成为答案。
A. Rager

14

从数学角度看,正方形是-矩形。如果数学家修改了正方形,使其不再遵守正方形协定,则它将变为矩形。

但是在OO设计中,这是一个问题。一个对象就是它的本质,这包括行为和状态。如果我持有一个正方形对象,但其他人将其修改为矩形,则不会因我自己的过错而违反正方形的约定。这会导致各种不良情况的发生。

这里的关键因素是可变性。形状一旦构造就可以改变吗?

  • 可变的:如果在构造后允许更改形状,则正方形与矩形不能具有is-a关系。矩形的收缩包括以下约束:相对的边必须具有相等的长度,而相邻的边则不必相等。正方形必须有四个相等的边。通过矩形接口修改正方形会违反正方形协定。

  • 不变的:如果形状一旦构建便无法更改,则正方形对象还必须始终满足矩形收缩。正方形可以与矩形具有is-a关系。

在这两种情况下,都可以根据具有一个或多个更改的状态要求正方形生成新形状。例如,可以说“根据该正方形创建一个新的矩形,但相对的A和C边的长度是其两倍。” 由于正在构建新对象,因此原始正方形继续遵守其合同。


1
This is one of those cases where the real world is not able to be modeled in a computer 100%。为什么这样?我们仍然可以使用正方形和矩形的功能模型。唯一的结果是,我们必须寻找一种更简单的构造来对这两个对象进行抽象。
西蒙·贝格

6
矩形和正方形之间的共同点远不止于此。问题在于矩形的标识和正方形的标识是其边长(以及每个相交处的角度)。最好的解决方案是使正方形从矩形继承,但使两者不变。
斯蒂芬

3
@斯蒂芬同意。实际上,不管子类型问题如何,使它们不变都是明智的。没有理由使它们可变-构造一个新的正方形或矩形要比变异它更容易,那么为什么要打开那些蠕虫呢?现在,您不必担心混叠/副作用,可以根据需要将它们用作映射/字典的键。有人会说“性能”,我会说“过早的优化”,直到他们实际测量并证明热点在形状代码中为止。
2014年

抱歉,来晚了,写答案时我很累。我改写了我的意思,这才是关键所在。

13

为何Square可以在您期望矩形的任何地方使用?

因为那是子类型含义的一部分(另请参见:Liskov替换原理)。您可以做到,需要能够做到这一点:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

实际上,使用OOP时,您始终会(有时甚至更隐含)执行此操作。

如果我们为Square重写SetWidth和SetHeight方法,那为什么会有问题呢?

因为您无法明智地为覆盖这些Square。因为正方形不能 “像任何矩形一样调整大小”。当矩形的高度改变时,宽度保持不变。但是,当正方形的高度改变时,宽度必须相应地改变。问题不仅在于调整大小,还在于在两个维度上都独立调整大小。


在许多语言中,您甚至都不需要该Rect r = s;行,您可以这样做,doSomethingWith(s)并且运行时将使用任何调用s来解析为任何虚拟Square方法。
Patrick M

1
@PatrickM您不需要任何具有子类型的理智的语言。明确地说,我将这一行包括在内。

因此,请覆盖setWidthsetHeight更改宽度和高度。
2014年

@ValekHalfHeart这正是我正在考虑的选项。

7
@ValekHalfHeart:这恰恰是违反Liskov替换原理的问题,它将困扰着您,并使您花费数个不眠之夜,试图在两年后发现一个奇怪的bug时却忘记了代码应该如何工作。
Jan Hudec 2014年

9

您所描述的内容违背了所谓的Liskov替代原则。LSP的基本思想是,无论何时使用特定类的实例,都应该始终能够交换该类任何子类的实例,而不会引入错误。

Rectangle-Square问题并不是介绍Liskov的好方法。它试图使用一个实际上很微妙的示例来解释一个广泛的原理,并且违反了所有数学中最常见的直观定义之一。出于这个原因,有人称其为椭圆圆问题,但就此而言,它仅稍好一点。更好的方法是使用我所谓的平行四边形-矩形问题稍微退后一步。这使事情更容易理解。

平行四边形是具有两对平行边的四边形。它还具有两对全等角。不难想象,平行线对象如下:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

想到矩形的一种常见方法是将其做成直角平行四边形。乍一看,这似乎使Rectangle成为从Parallelogram继承的不错选择,因此您可以重用所有这些美味的代码。然而:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

为什么这两个函数会在Rectangle中引入错误?问题在于您无法更改矩形中的角度:它们被定义为始终为90度,因此该接口实际上不适用于从平行四边形继承的Rectangle。如果我将Rectangle交换为期望具有平行四边形的代码,并且该代码试图更改角度,则几乎肯定会出现bug。我们采用了在子类中可写的东西,并将其设置为只读,这是对Liskov的违反。

现在,这如何将其应用于正方形和矩形?

当我们说您可以设置一个值时,通常意味着要比仅可以向其中写入值要强一些。我们暗示一定程度的排他性:如果您设置一个值,然后再排除一些特殊情况,它将保持该值,直到您再次设置它。可以写入但不保持设置值的值有很多用途,但是在很多情况下,取决于值在设置后保持不变。这就是我们遇到另一个问题的地方。

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

我们的Square类继承了Rectangle的错误,但是有一些新错误。setSideA和setSideB的问题在于,这两个都不再是真正可设置的:您仍然可以将一个值写入其中一个,但是如果写入另一个值,则该值会从您的下方更改。如果我将其交换为代码中的平行四边形,而这取决于能够彼此独立地设置边,那么它将会变得很奇怪。

这就是问题所在,这就是为什么将Rectangle-Square用作Liskov的介绍会存在问题。Rectangle-Square取决于能够写入内容设置内容之间的差异这与能够设置内容与将其设置为只读相比要细微得多。Rectangle-Square仍然具有作为示例的价值,因为它记录了必须提防的相当普遍的陷阱,但是不应将其用作入门示例。首先让学习者了解一些基础知识,然后再对这些基础知识进行更严格的介绍。


8

子类型与行为有关。

为了使type B成为type 的子类型A,它必须A以相同的语义支持类型支持的每个操作(花哨的“行为”)。使用的理由每个B是一个不会工作-行为的兼容性拥有最终解释权。大多数情况下,“ B是一种A”与“ B表现得像A”重叠,但并非总是如此

一个例子:

考虑实数集。在任何一种语言,我们能指望他们支持的操作+-*,和/。现在考虑正整数集({1,2,3,...})。显然,每个正整数也是一个实数。但是正整数的类型是实数类型的子类型吗?让我们看一下这四个操作,看看正整数的行为与实数相同:

  • +:我们可以添加正整数而没有问题。
  • -:并非所有的正整数相减都会产生正整数。例如3 - 5
  • *:我们可以将正整数相乘而没有问题。
  • /:我们不能总是除以正整数并获得正整数。例如5 / 3

因此,尽管正整数是实数的子集,但它们不是子类型。对于有限大小的整数,可以使用类似的参数。显然,每个32位整数也是64位整数,但是32_BIT_MAX + 1每种类型的结果都不同。因此,如果我给您一些程序,并且您将每个32位整数变量的类型更改为64位整数,则该程序很有可能会表现出不同的行为(这几乎总是表示错误)。

当然,您可以定义+32位整数,以便结果是64位整数,但是现在每次添加两个32位数字时,您都必须保留64位空间。根据您的内存需求,这可能会接受,也可能无法接受。

为什么这么重要?

正确的程序很重要。可以说,这是程序拥有的最重要的属性。如果某个程序对于某种类型是正确的A,则确保该程序对于某些子类型将继续正确的唯一方法BB行为A各方面都一样。

因此,您具有的类型Rectangles,其规格说明其侧面可以独立更改。您编写了一些程序,这些程序使用Rectangles并假定实现遵循规范。然后,您引入了一个子类型Square,该子类型的边不能独立调整大小。结果,现在大多数调整矩形大小的程序都是错误的。


6

如果Square是Rectangle的一种类型,那么为什么Square无法从Rectangle继承呢?还是为什么设计不好?

首先,问问自己为什么您认为正方形是矩形。

当然,大多数人是在小学学习的,这似乎很明显。矩形是具有90度角的4边形,而正方形则具有所有这些特性。正方形不是矩形吗?

但事实是,这完全取决于您对对象进行分组的初始标准,查看这些对象的上下文。在几何中,形状是根据其点,线和角度的属性进行分类的。

因此,在您甚至说“正方形是矩形的一种”之前,您首先要问自己,这是否基于我关心的标准

在大多数情况下,这根本不是您所关心的。大多数对形状建模的系统(例如GUI,图形和视频游戏)并不首先关注对象的几何分组,而是行为。您是否曾经在一个系统上工作过,所以从几何意义上来说正方形是矩形的类型很重要。知道它有4个侧面和90度角,那还能给您什么?

您不是在建模静态系统,而是在建模将要发生的动态系统(将要创建,销毁,更改,绘制形状等)。在这种情况下,您关心的是对象之间的共享行为,因为您最关心的是形状可以做什么,必须保持哪些规则才能保持一致的系统。

在这种情况下,正方形绝对不是矩形,因为控制如何更改正方形的规则与矩形不同。所以它们不是同一类型的东西。

在这种情况下,请勿照此建模。你怎么会 除了不必要的限制外,它无济于事。

仅当我们创建Square对象,并且重写Square的SetWidth和SetHeight方法时,它才可用,为什么会有任何问题呢?

如果您这样做,尽管实际上是在代码中声明它们不是同一回事。您的代码会说一个正方形的行为和一个矩形的行为一样,但是它们仍然相同。

在您所关心的上下文中,它们显然是不同的,因为您只是定义了两种不同的行为。那么,如果它们只是在您不关心的情况下是相似的,为什么还要假装它们是相同的呢?

当开发人员进入他们希望建模的领域时,这突出了一个重大问题。在开始考虑域中的对象之前,弄清您感兴趣的上下文非常重要。您对什么方面感兴趣。数千年前,希腊人关心线条和形状天使的共同属性,并根据这些属性进行分组。这并不意味着如果您不在乎您就必须继续进行该分组(在99%的时间中,您不需要在软件中建模)。

这个问题的很多答案都集中在子类型上,即关于将行为归为“分组规则”

但是了解这一点非常重要,以至于您不只是为了遵守规则而这样做。您之所以这样做,是因为在大多数情况下,这也是您真正关心的。您不在乎正方形和矩形是否共享相同的内部天使。您关心它们在仍然是正方形和矩形的情况下可以做什么。您关心对象的行为,因为您正在建模一个系统,该系统专注于根据对象的行为规则更改系统。


如果类型变量Rectangle仅用于表示,则类Square可以继承Rectangle并完全遵守其契约。不幸的是,许多语言在封装值的变量和标识实体的变量之间没有任何区别。
2014年

可能,但是为什么要首先打扰。矩形/正方形问题的重点不是试图弄清楚如何使“正方形就是矩形”关系起作用,而是要意识到在使用对象的上下文中该关系实际上不存在(行为上),并警告您不要在您的网域上强加无关紧要的关系。
Cormac Mulhall 2014年

或换种说法:请勿尝试弯曲汤匙。这不可能。取而代之的是,只想了解事实,那就没有汤匙。:-)
Cormac Mulhall 2014年

1
如果存在某些只能在平方上执行的操作,那么拥有Square从不可变类型继承的不可变Rectnagle类型可能会很有用。作为该概念的一个现实示例,考虑一种ReadableMatrix类型[基本类型是一个矩形数组,它可能以各种方式存储,包括稀疏地]和一种ComputeDeterminant方法。它可能是有意义的有ComputeDeterminant工作只用ReadableSquareMatrix这与派生类型ReadableMatrix,我会认为是一个示例Square从推导Rectangle
超级猫

5

如果Square是Rectangle的一种类型,那么为什么Square无法从Rectangle继承呢?

问题在于认为,如果事物在现实中以某种方式关联,则它们在建模后必须以完全相同的方式关联。

建模中最重要的事情是识别共同的属性和共同的行为,在基本类中定义它们,并在子类中添加其他属性。

您的示例的问题是,这是完全抽象的。只要没有人知道您打算将该类用于什么目的,就很难猜测您应该进行哪种设计。但是,如果您真的只想拥有高度,宽度和调整大小,则更合乎逻辑:

  • 将Square定义为基类,使用width参数并resize(double factor)通过给定因子调整宽度
  • 定义Rectangle类和Square的子类,因为它添加了另一个属性height,并覆盖了它的resize函数,该函数调用super.resize然后按给定因子调整高度

从编程的角度来看,Square中没有任何东西,而Rectangle没有。没有将Square作为Rectangle的子类的感觉。


+1仅仅因为正方形是数学中的一种特殊矩形,并不意味着它在OO中是相同的。
罗维斯(Lovis)

1
正方形是正方形,矩形是矩形。它们之间的关系也应该保留在建模中,否则您的模型会很差。真正的问题是:1)如果使它们可变,就不再为正方形和矩形建模;2)假设仅仅因为两种对象之间存在某种“是”关系,您就可以随意地用另一个代替。
2014年

4

因为通过LSP,在两者之间创建继承关系并覆盖setWidthsetHeight确保平方具有相同的作用,所以会引起混淆和非直观行为。假设我们有一个代码:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

但是如果方法createRectangle返回了Square,因为有可能要Square继承自Rectange。然后期望就破了。在这里,使用此代码,我们期望设置width或height只会分别导致width或height的更改。OOP的要点是,当您使用超类时,您对其下的任何子类都不了解。并且,如果子类改变了行为,从而违反了我们对超类的期望,则很可能会出现错误。而且这类错误很难调试和修复。

有关OOP的主要思想之一是,它是行为,而不是继承的数据(这也是IMO的主要误解之一)。而且,如果您查看正方形和矩形,它们本身没有可与继承关系关联的行为。


2

LSP的意思是,任何从中继承的都Rectangle必须是Rectangle。也就是说,它应该做任何事情Rectangle

可能的for文档Rectangle被写成说一个Rectanglenamed 的行为r如下:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

如果您的Square没有相同的行为,那么它的行为就不会像Rectangle。因此LSP表示它一定不能继承Rectangle。语言不能强制执行此规则,因为它不能阻止您在方法重写中做错事,但这并不意味着“可以,因为语言允许我重写方法”是执行此操作的令人信服的论点!

现在,可以Rectangle不暗示上面的代码打印10的方式编写文档,在这种情况下,您Square可能是Rectangle。您可能会看到说明如下的文档:“这执行X。此外,此类中的实现执行Y”。如果是这样,那么您就有很好的理由从类中提取接口,并区分接口所保证的内容以及该类所保证的内容。但是,当人们说“可变的正方形不是可变的矩形,而不变的正方形是不变的矩形”时,他们基本上是在假设上述内容确实是可变矩形的合理定义的一部分。


这似乎只是重复一个明确的点答案发布了5个小时前
蚊蚋

@gnat:您是否希望我将其他答案编辑得如此简洁?;-)我认为,如果不删除其他回答者可能认为回答该问题所必需的要点,而我却认为没有必要,我将无法消除。
史蒂夫·杰索普


1

子类型以及OO编程的扩展通常依赖于Liskov替换原理,即如果A <= B,则在需要B的任何地方都可以使用A类型的任何值。假定所有子类都具有此属性(如果没有,则子类型有错误,需要修复)。

但是,事实证明,该原理对于大多数代码而言是不现实/不具有代表性的,或者实际上是无法满足的(在不平凡的情况下)!这个被称为矩形问题或圆形椭圆问题(http://en.wikipedia.org/wiki/Circle-ellipse_problem)的问题是一个很难实现的著名例子。

请注意,我们可以实现越来越多的等效观察到的Squares和Rectangles,但只能通过扔掉越来越多的功能,直到区别无用为止。

例如,请参见http://okmij.org/ftp/Computation/Subtyping/

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.