为什么不可变对象允许符合 Liskov 替换原则



我想展示一个矩形和正方形的例子:

    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基类上有SetWidthSetHeight方法;这似乎是完全合乎逻辑的。但是,如果您的Rectangle引用指向Square,则SetWidthSetHeight没有意义,因为设置一个会更改另一个以匹配它。在这种情况下,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

最新更新