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

我听说人们说:


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


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


如果您有SetWidth和SetHeight方法在您的Rectangle基类上
,如果Rectangle引用指向一个Square,则SetWidth和
SetHeight都没有意义,因为设置一个会更改
另一个来匹配它。在这种情况下,Square无法通过矩形进行Liskov替换
测试,而让Square继承自矩形的抽象是不好的。


有人可以解释上面的内容吗?争论?同样,如果我们在Square中重写SetWidth和SetHeight方法,是否不能解决此问题?

我也听说过:


真正的问题是,我们不是在建模矩形,而是
“可接受的矩形”,即,其宽度或高度可以在创建后修改
的矩形(并且我们仍然认为它是相同的
目的)。如果我们以这种方式看待矩形类,很显然
正方形不是“可接受的矩形”,因为正方形不能被整形并且仍然是正方形(通常)。从数学上来说,我们
看不到问题,因为可变性甚至在数学上下文中都没有意义


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

评论

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

回想一下常识,正方形是一个矩形,因此,如果不能在需要矩形的地方使用正方形类的对象,则可能仍然是应用程序设计的缺陷。

我认为更好的问题是,为什么我们甚至需要Square?就像有两支笔。一支蓝色的笔和一支红色的蓝色,黄色或绿色的笔。蓝色笔是多余的-在正方形的情况下更是如此,因为它没有成本优势。

@eBusiness它的抽象性使它成为一个很好的学习问题。能够独立于特定用例而认识到子类型化的不良用途很重要。

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

#1 楼

基本上,我们希望事情表现得合理。

请考虑以下问题:

我得到了一组矩形,我想将其面积增加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替代原则可能会引入非常细微的错误,这些错误只有在写完后几年才会被发现,到那时修复此违规将破坏一堆事情,而未修复它会激怒您的最大客户。

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

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

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

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

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

评论


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

–el.pescado
2014年5月7日在7:54

巧妙运用故事来说明要点

–罗里·亨特(Rory Hunter)
2014年5月7日在8:24

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

–罗伊T.
2014年5月7日12:00



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

– cHao
2014年5月7日13:03



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

–斯蒂芬
2014年5月7日13:06

#2 楼

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

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

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

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

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

这是协方差与对数方差的问题。

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


只返回那些超类将返回-即,返回类型必须是超类方法的返回类型的子类型。返回值是协变量。
接受超类型将接受的所有值-也就是说,参数类型必须是超类方法的参数类型的超类型。参数是反变量。

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

如果Rectangle具有用于宽度和高度的单独的设置方法,Square将不是一个好的子类。

同样,如果您使某些方法在参数中是协变的,就像在矩形上使用compareTo(Rectangle),在方形上使用compareTo(Square)一样,将方形用作矩形会遇到问题。

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

评论


“如果您的所有对象都是不可变的,就没有问题” –在此问题的上下文中这显然是不相关的声明,其中明确提到了宽度和高度的设置方法

– gna
2014年5月7日11:38



我发现这很有趣,即使它与“显然无关”

– Jesvin Jose
2014年5月7日在12:03

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

–Doval
2014年5月7日12:52

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

–帕特里克M
2014年5月7日15:50

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

– Nitin Bhide
2014年5月9日13:59

#3 楼

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

我想我可能会简单地谈论矩形和正方形的具体问题,而不是谈论

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


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

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

评论


如果形状对象是不可变的,则可能具有一个包含边界框的IShape类型,并且可以对其进行绘制,缩放和序列化,并且具有IPolygon子类型,该子类型具有一种报告顶点数量的方法和一种返回IEnumerable的方法。 <点>。然后可以具有衍生自IPolygon,IRhombus和IRectangle的IQuadrilateral亚型,并由此衍生出ISquare衍生自IRhombus和IRectangle。可变性会把所有东西扔到窗外,并且多重继承不适用于类,但是我认为使用不可变的接口就可以了。

–超级猫
2014年11月17日23:10

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

–马克·赫德
15年5月13日在2:13

很好的答案!应该更高。

– andrew.fox
16年1月8日在19:57

@MarkHurd-这不是YAGNI的问题:建议的基于继承的层次结构的形状类似于所描述的分类法,但无法编写以保证定义它的关系。 IRhombus如何保证从IPolygon定义的Enumerable 返回的所有Point对应于相等长度的边?因为仅IRhombus接口的实现不能保证具体的对象是菱形,所以继承不能解决问题。

– A. Rager
17-6-28 17:18



#4 楼

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

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

这里的关键因素是可变性。可变形状是否可以更改?



可变:如果形状一旦被允许更改,正方形就不能与矩形具有is-a关系。矩形的收缩包括以下约束:相对的边必须具有相等的长度,而相邻的边则不必相等。正方形必须有四个相等的边。通过矩形接口修改正方形可能会违反正方形收缩。
不可修改:如果形状一旦构造便无法更改,则正方形对象还必须始终满足矩形收缩。正方形可以与矩形具有is-a关系。

在两种情况下,都可以要求正方形根据其状态进行一次或多次更改以产生新形状。例如,人们可能会说“基于该正方形创建一个新的矩形,不同的是,相对的A和C边的长度是其两倍。”由于正在建造新对象,因此原始广场继续遵守其合同。

评论


这是无法在计算机中100%建模真实世界的情况之一。为什么这样?我们仍然可以使用正方形和矩形的功能模型。唯一的结果是,我们必须寻找一种更简单的构造来对这两个对象进行抽象。

–西蒙·贝格(Simon Bergot)
2014年5月7日在6:16

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

–斯蒂芬
2014年5月7日在7:29

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

–Doval
2014年5月7日晚上11:35

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

–user22815
2014年5月7日14:25

#5 楼


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


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

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


您实际上是这样做的



一直(有时甚至更隐含)。如果我们重写Square的SetWidth和SetHeight方法,为什么会出现问题?


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

评论


在许多语言中,您甚至不需要Rect r = s。行中,您可以只执行doSomethingWith(s),运行时将使用对的任何调用来解析为任何虚拟Square方法。

–帕特里克M
2014年5月7日15:48

@PatrickM您不需要任何具有子类型的理智的语言。明确地说,我包括了这一行进行阐述。

–user7043
2014年5月7日15:51

因此,覆盖setWidth和setHeight可以同时更改宽度和高度。

–接近黑暗鱼
2014年5月7日下午16:40

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

–user7043
2014年5月7日下午16:43

@ValekHalfHeart:正是这违反了Liskov替代原则,这将困扰着您,并使您度过不眠之夜,两年后,当您忘记了代码应如何工作时,试图找到一个奇怪的错误。

– Jan Hudec
2014年5月7日21:27



#6 楼

子类型化与行为有关。

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

示例:

考虑实数集。无论使用哪种语言,我们都可以期望它们支持+-*/运算。现在考虑正整数集合({1、2、3,...})。显然,每个正整数也是一个实数。但是正整数的类型是实数类型的子类型吗?让我们看一下这四个操作,看看正整数的行为与实数是否相同:



+:我们可以添加正整数而没有问题。

-:并非所有的正整数相减都会产生正整数。例如。 3 - 5

*:我们可以乘以正整数而没有问题。

/:我们不能总是将正整数相除并得到正整数。例如。 5 / 3

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

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

为什么这么重要?

程序正确很重要。可以说,它是程序拥有的最重要的属性。如果某个程序对于某些类型A是正确的,则保证该程序对于某些子类型B仍将正确的唯一方法是,B的行为在各个方面都像A

所以您拥有Rectangles,其规格说明其侧面可以独立更改。您编写了一些使用Rectangles的程序,并假定实现遵循规范。然后,您引入了一个名为Square的子类型,该子类型的边无法独立调整大小。结果,现在大多数调整矩形大小的程序都会出错。

#7 楼

您所描述的内容与所谓的Liskov替代原理不符。 LSP的基本思想是,只要您使用特定类的实例,就应该始终能够交换该类任何子类的实例,而不会引入错误。

Rectangle-平方问题并不是介绍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交换为期望具有平行四边形的代码,并且该代码尝试更改角度,则几乎肯定会存在错误。我们已经采用了子类中可写的内容并将其设为只读,这违反了Liskov。

现在,这如何将其应用于Squares和Rectangles?

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

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

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

#8 楼


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


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

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

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

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

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

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

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

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


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


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

在您所关心的上下文中,它们显然并不相同,因为您只定义了两个不同的行为。那么,如果它们只是在您不关心的情况下是相似的,那么为什么要假装它们是相同的呢?在开始考虑域中的对象之前,弄清您感兴趣的上下文非常重要。您对什么方面感兴趣。几千年前,希腊人关心线条和形状天使的共同属性,并根据这些属性进行分组。这并不意味着如果您不关心它,就不会被迫继续进行分组(在99%的时间中,您将不在乎软件建模)。

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

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

评论


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

–超级猫
2014年5月8日22:45

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

– Cormac Mulhall
2014年5月9日在9:31

或换种说法:请勿尝试弯曲汤匙。这不可能。取而代之的是,只想认识真相,那就没有汤匙。 :-)

– Cormac Mulhall
2014年5月12日晚上8:39

如果存在某些只能对平方执行的操作,则具有从不可变的Rectnagle类型继承的不可变Square类型可能会很有用。作为该概念的一个现实示例,考虑一个ReadableMatrix类型(基本类型是一个矩形阵列,可以稀疏地存储它的各种方式)和ComputeDeterminant方法。仅将ComputeDeterminant与从ReadableMatrix派生的ReadableSquareMatrix类型一起使用可能是有意义的,我认为这是从矩形派生的Square的示例。

–超级猫
2014年5月12日13:23

#9 楼


如果正方形是矩形的,那么为什么正方形不能从矩形继承呢?


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

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

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


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

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

评论


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

–爱神
2014年5月8日在16:27

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

–Doval
2014年5月9日12:00

#10 楼

因为通过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,因为由于SquareRectange继承而来是可能的。然后期望就破了。在这里,使用此代码,我们期望设置width或height只会分别导致width或height的变化。 OOP的要点是,当您使用超类时,您对其下的任何子类都不了解。并且,如果子类改变了行为,从而违反了我们对超类的期望,则很可能会发生错误。而且这类错误很难调试和修复。

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

#11 楼

LSP说的是,从Rectangle继承的任何东西都必须是Rectangle。也就是说,它应该执行Rectangle的所有操作。

Rectangle的文档可能写成说名为Rectangler的行为如下:

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


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

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

评论


这似乎只是重复5小时前发布的答案中解释的要点

– gna
2014年5月7日在16:29

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

–史蒂夫·杰索普(Steve Jessop)
2014年5月7日16:31



meta.workplace.stackexchange.com/questions/2562/…

– gna
2014年5月7日16:50

#12 楼

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

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

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

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