我是设计和学习设计原则的新手。
它说从矩形导出正方形是违反里氏替换原则的典型例子。
如果是这样,正确的设计应该是什么?
答案取决于可变性。如果你的 rectangle 和 square 类是不可变的,那么Square
它确实是一个子类型,Rectangle
从 second 派生 first 是完全可以的。否则,Rectangle
两者Square
都可以暴露一个IRectangle
没有突变体的,但从另一个派生一个是错误的,因为这两种类型都不是另一种的正确子类型。
我相信推理是这样的:
假设您有一个接受矩形并调整其宽度的方法:
public void SetWidth(Rectangle rect, int width)
{
rect.Width = width;
}
考虑到矩形是什么,假设这个测试会通过应该是完全合理的:
Rectangle rect = new Rectangle(50, 20); // width, height
SetWidth(rect, 100);
Assert.AreEqual(20, rect.Height);
...因为更改矩形的宽度不会影响其高度。
但是,假设您从 Rectangle 派生了一个新的 Square 类。根据定义,正方形的高度和宽度始终相等。让我们再次尝试该测试:
Rectangle rect = new Square(20); // both width and height
SetWidth(rect, 100);
Assert.AreEqual(20, rect.Height);
该测试将失败,因为将正方形的宽度设置为 100 也会改变其高度。
因此,从 Rectangle 派生 Square 违反了 Liskov 的替换原则。
“is-a”规则在“现实世界”中是有意义的(正方形绝对是一种矩形),但在软件设计领域并不总是如此。
编辑
要回答您的问题,正确的设计可能应该是 Rectangle 和 Square 都派生自一个通用的“Polygon”或“Shape”类,它不强制执行有关宽度或高度的任何规则。
我不同意从矩形派生正方形必然违反 LSP。
在 Matt 的示例中,如果您的代码依赖于独立的宽度和高度,那么它实际上违反了 LSP。
但是,如果您可以在代码中的任何地方用矩形代替正方形而不破坏任何假设,那么您就没有违反 LSP。
因此,这实际上归结为抽象矩形在您的解决方案中的含义。
我最近一直在为这个问题苦苦挣扎,并想我会把我的帽子加到戒指上:
public class Rectangle {
protected int height;
protected int width;
public Rectangle (int height, int width) {
this.height = height;
this.width = width;
}
public int computeArea () { return this.height * this.width; }
public int getHeight () { return this.height; }
public int getWidth () { return this.width; }
}
public class Square extends Rectangle {
public Square (int sideLength) {
super(sideLength, sideLength);
}
}
public class ResizableRectangle extends Rectangle {
public ResizableRectangle (int height, int width) {
super(height, width);
}
public void setHeight (int height) { this.height = height; }
public void setWidth (int width) { this.width = width; }
}
注意最后一节课,ResizableRectangle
。通过将“可调整大小”移动到子类中,我们可以重用代码,同时实际改进我们的模型。可以这样想:一个正方形不能在保持正方形的同时自由调整大小,而非正方形的矩形可以。但是,并非所有矩形都可以调整大小,因为正方形是矩形(并且不能在保持其“身份”的同时自由调整大小)。(o_O) 所以创建一个不可调整大小的基类是有意义的Rectangle
,因为这是一些矩形的额外属性。
问题在于,所描述的实际上不是“类型”,而是累积的涌现属性。
你真正拥有的只是一个四边形,“正方形”和“矩形”都只是从角度和边的属性中衍生出来的伪影。
“正方形”(甚至是矩形)的整个概念只是对象的属性集合的抽象表示,这些属性相对于彼此和所讨论的对象,而不是对象本身的类型。
这是在无类型语言的上下文中思考问题的地方,因为不是类型决定了它是否是“正方形”,而是对象的实际属性决定了它是否是“正方形”。
我想如果你想进一步抽象,你甚至不会说你有一个四边形,而是你有一个多边形,甚至只是一个形状。
假设我们有 Rectangle 类,它有两个(为简单起见是公共的)属性 width,height。我们可以改变这两个属性:r.width=1, r.height=2。
现在我们说 Square is_a Rectangle。但是,尽管声称“正方形会表现得像矩形”,但我们不能在正方形对象上设置 .width=1 和 .height=2 (如果你设置了高度,你的类可能会调整宽度,反之亦然)。因此,至少在一种情况下,Square 类型的对象的行为不像 Rectangle,因此您不能(完全)替换它们。
我相信 OOD/OOP 技术的存在使软件能够代表现实世界。在现实世界中,正方形是具有相等边的矩形。正方形是正方形只是因为它有相等的边,而不是因为它决定是正方形。因此,OO程序需要处理它。当然,如果实例化对象的例程希望它是正方形的,它可以指定长度属性和宽度属性等于相同的量。如果使用该对象的程序稍后需要知道它是否为正方形,则只需询问它即可。该对象可能有一个名为“Square”的只读布尔属性。当调用例程调用它时,对象可以返回(长度 = 宽度)。现在,即使矩形对象是不可变的,情况也是如此。此外,如果矩形确实是不可变的,Square 属性的值可以在构造函数中设置并使用它来完成。为什么这是一个问题?LSP 要求子对象是不可变的才能应用,而作为矩形的子对象的正方形通常被用作其违规的示例。但这似乎不是一个好的设计,因为当使用例程调用对象作为“objSquare”时,必须知道它的内部细节。如果不关心矩形是不是正方形不是更好吗?那是因为矩形的方法无论如何都是正确的。是否有更好的示例来说明何时违反 LSP?但这似乎不是一个好的设计,因为当使用例程调用对象作为“objSquare”时,必须知道它的内部细节。如果不关心矩形是不是正方形不是更好吗?那是因为矩形的方法无论如何都是正确的。是否有更好的示例来说明何时违反 LSP?但这似乎不是一个好的设计,因为当使用例程调用对象作为“objSquare”时,必须知道它的内部细节。如果不关心矩形是不是正方形不是更好吗?那是因为矩形的方法无论如何都是正确的。是否有更好的示例来说明何时违反 LSP?
还有一个问题:如何使对象不可变?是否有可以在实例化时设置的“不可变”属性?
我找到了答案,这正是我所期望的。由于我是一名 VB .NET 开发人员,这就是我感兴趣的内容。但是跨语言的概念是相同的。在 VB .NET 中,通过将属性设为只读来创建不可变类,并使用 New 构造函数允许实例化例程在创建对象时指定属性值。您还可以对某些属性使用常量,它们将始终相同。从创建开始,对象是不可变的。
它非常简单:) 类(派生链中的第一个)越“基础”应该是最通用的。
例如形状 -> 矩形 -> 正方形。
这里的正方形是矩形的特例(具有约束尺寸),矩形是形状的特例。
换一种说法——使用“是”测试。乡绅是一个长方形。但矩形并不总是正方形。