我想用矩形和正方形显示一个例子:
class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int area() {
return width * height;
}
}
class Square extends Rectangle{
@Override
public void setWidth(int width){
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height){
super.setHeight(height);
super.setWidth(height);
}
}
public class Use {
public static void main(String[] args) {
Rectangle sq = new Square();
LSPTest(sq);
}
public static void LSPTest(Rectangle rec) {
rec.setWidth(5);
rec.setHeight(4);
if (rec.area() == 20) {
// do Something
}
}
}
如果我在方法LSPTest中替换Square
的实例而不是Rectangle
,我的程序的行为将会改变。这与LSP相反。
我听说,不可变对象允许解决这个问题。但为什么呢?
我改变了榜样。
我在Rectangle
中添加了构造函数:
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
然后,我改变了二传手:
public Rectangle setWidth(int width) {
return new Rectangle(width, this.height);
}
public Rectangle setHeight(int height) {
return new Rectangle(this.width, height);
}
现在,Square
看起来像是:
class Square{
public Square() {
}
public Square(int width, int height) {
super(width, height);
}
@Override
public Rectangle setWidth(int width) {
return new Rectangle(width, width);
}
@Override
public Rectangle setHeight(int height) {
return new Rectangle(height, height);
}
}
这是客户端代码:
public class Use {
public static void main(String[] args) {
Rectangle sq = new Square(4, 4);
LSPTest(sq);
}
public static void LSPTest(Rectangle rec) {
rec = rec.setHeight(5);
if (rec.area() == 20) {
System.out.println("yes");
}
}
}
同样的问题仍然存在。对象本身是否被更改或返回新对象的区别是什么。对于基类和子类,程序的行为仍然不同。
答案 0 :(得分:2)
From here我抓住了这些引用(强调我的):
想象一下,您的
SetWidth
基类上有SetHeight
和Rectangle
个方法;这似乎完全符合逻辑。但是,如果您的Rectangle
引用指向Square
,则SetWidth
和SetHeight
没有意义,因为设置一个会更改另一个以匹配它。在这种情况下,Square
使Rectangle
Liscov替换测试失败,并且从Square
继承Rectangle
的抽象是错误
...和...
LSP指示的是子类型行为应该与基本类型规范中定义的基本类型行为匹配。如果矩形基本类型规范说高度和宽度可以独立设置,那么LSP说广场不能是矩形的子类型。 如果矩形规范说矩形是不可变的,那么正方形可以是矩形的子类型。这是关于维持为基本类型指定的行为的子类型。
我想如果你有一个像
这样的构造函数,它会工作Square(int side){
super(side,side);
...
}
因为没有办法改变不可变的东西,所以没有固定器。广场将永远是正方形。
但是应该可以在两者之间建立一个不违反LSP的关系,也不会强迫您使用不可变对象。我们只是错了。
在数学中,正方形可以被认为是一种矩形。事实上,它是一种更具体的矩形。天真地,做Square extends Rectangle
似乎合乎逻辑,因为矩形似乎是super
。但是,拥有子类的目的不是创建现有类的较弱版本,而是应该增强功能。
为什么不能这样:
class Square{
void setSides(int side);
Boundary getSides();
}
class Rectangle extends Square{
//Overload
void setSides(int width, int height);
@Override
Boundary getSides();
}
我还想指出 Setters 适用于设置。下面的代码非常糟糕,因为你基本上创建了一个不会按照它所说的那样做的方法。
public Rectangle setWidth(int width) {
return new Rectangle(width, this.height);
}
答案 1 :(得分:0)
问题在于合同,即使用Rectangle
的程序员的期望。
合同是您可以setWidth(15)
,之后getWidth()
会返回15
,直到您使用不同的价值执行另一个setWidth
。
从setHeight
的角度来看,这意味着它不得更改height
。继续这一思路,并且设置者的合同是“将此属性更新为参数值,保持所有其他属性不变”。
现在在Square
,新的不变量getWidth() == getHeight()
强制setWidth
也设置了高度,并且vo:setHeight
的合同被违反。
当然,您可以在Rectangle.setWidth()
的合同(即方法文档)中明确说明如果调用setHeight()
,宽度可能会发生变化。
但是现在你在setWidth
上的合同是没用的:它会设置宽度,在调用setHeight
之后可能会保持不变,这取决于子类可能决定做什么。
事情会变得更糟。假设你推出了你的Rectangle
,人们会抱怨一下这个不寻常的合同,但一切都很好。
但是现在有人来了并想要添加一个新的子类OriginCenteredRectangle
。现在改变宽度也需要更新x和y。有趣的是向用户解释您必须修改基类的合同,以便可以添加另一个子类(只有10%的用户需要)。
实践证明,OriginCenteredRectangle
问题远比该例子所表明的愚蠢更常见
此外,实践表明,程序员通常不了解完整的合同,并开始编写可更新的子类,这些子类会在期望面前飞行,从而导致细微的错误。
所以大多数编程语言社区最终决定你需要价值类;从我在C ++和Java中看到的,这个过程需要十年或两年。
现在使用不可变类,你的setter突然变得不同了:
void setWidth(width)
变为Rectangle setWidth(width)
。即如果不编写setter,则编写返回宽度不同的新对象的函数
这在Square
完全可以接受:setWidth
保持不变,仍然会返回Rectangle
。
当然,您需要一个函数来返回不同的方格,因此Square添加函数Square Square.setSize(size)
。
您仍然希望MutableRectangle
类只需构建Rectangles
而无需创建新副本 - 您将Rectangle toRectangle()
执行构造函数调用。 (MutableRectangle
的另一个名称是RectangleBuilder
,它描述了一个更有限的推荐使用该类。选择你的风格 - 我个人认为MutableRectangle
很好,只是那个大多数子类化尝试都会失败,所以我会考虑将其设为final
。)