我想展示一个矩形和正方形的例子:
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
}
}
}
如果我替换Square
的实例而不是方法LSPTest中的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");
}
}
}
同样的问题依然存在。是更改对象本身还是返回新对象有什么区别。对于基类和子类,程序的行为仍然不同。
从这里我抓住了这些引号(强调我的(:
假设您在
Rectangle
基类上有SetWidth
和SetHeight
方法;这似乎是完全合乎逻辑的。但是,如果您的Rectangle
引用指向Square
,则SetWidth
和SetHeight
没有意义,因为设置一个会更改另一个以匹配它。在这种情况下,Square
Rectangle
的 Liskov 替换测试失败,并且从Rectangle
继承Square
抽象是一个糟糕的抽象。
。和。。。
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();
}
我还想指出,二传手是用来设置的。下面的代码很糟糕,因为你基本上创建了一个方法,它不会按照它所说的去做。
public Rectangle setWidth(int width) {
return new Rectangle(width, this.height);
}
问题出在合同上,即使用您的Rectangle
的程序员的期望。
合同是你可以做一个setWidth(15)
之后getWidth()
会返回15
,直到你做另一个具有不同值的setWidth
。
从setHeight
的角度来看,这意味着它一定不能改变height
。继续这个思路,二传手的合约是"将此属性更新为参数值,并保持所有其他属性不变"。
现在在Square
,新的不变getWidth() == getHeight()
迫使setWidth
也设定高度,瞧:违反了setHeight
的契约。
当然,你可以在Rectangle.setWidth()
的合约(即方法文档(中明确声明,如果调用setHeight()
,宽度可能会改变。
但是现在你在setWidth
上的合约是毫无用处的:它将设置宽度,在调用setHeight
后可能会或可能不会保持不变,这取决于子类可能决定做什么。
事情可能会变得更糟。假设你推出你的Rectangle
,人们会抱怨二传手的不寻常合同,但除此之外一切都很好。
但是现在有人来了,想添加一个新的子类,OriginCenteredRectangle
.现在更改宽度还需要更新 x 和 y。愉快地向用户解释您必须修改基类的协定,以便可以添加另一个子类(只有 10% 的用户需要(。
实践表明,OriginCenteredRectangle
问题远比这个例子的愚蠢所表明的要普遍得多。
此外,实践表明,程序员通常不知道完整的合约,并开始编写可更新的子类,这些子类与期望背道而驰,从而导致微妙的错误。
因此,大多数编程语言社区最终决定您需要值类;从我在C++和Java中看到的情况来看,这个过程需要十年或二十年的时间。
现在有了不可变的类,你的二传手突然看起来不一样了: void setWidth(width)
变得Rectangle setWidth(width)
.也就是说,你不写 setter,你写的函数返回一个不同宽度的新对象。
这在Square
中是完全可以接受的:setWidth
保持不变,并且仍然返回必须返回Rectangle
。当然,您需要一个函数来返回不同的正方形,因此Square添加了函数Square Square.setSize(size)
。
您仍然需要一个 MutableRectangle
类来构造Rectangles
而不必创建新副本 - 您将拥有执行构造函数调用的Rectangle toRectangle()
。(MutableRectangle
的另一个名称是 RectangleBuilder
,它描述了对该类的更有限的推荐用法。 选择你的风格 - 就个人而言,我认为MutableRectangle
很好,只是大多数子类化尝试都会失败,所以我会考虑让它final
。