Liskov替换原则 - 如何建模正方形和矩形

5

可能是重复问题:
从矩形派生正方形是否违反了里氏替换原则?

应用LSP,有人能给出一个正方形和矩形的实现吗?

我读过这本书 - 《Head First面向对象分析与设计》,他们说如果正方形继承自矩形,则违反了LSP但没有适当的实现。

有人想试试吗?


1
这是经典的 LSP 示例,通常存在问题:矩形具有宽度和长度,但正方形只需要一个“边长”:https://dev59.com/YHNA5IYBdhLWcg3wQ7i4 - wkl
或者从四边形(Quadrilateral)继承,而四边形又继承自形状(Shape)。所有的四边形都有四条边、四个角等。 - David R Tribble
@Loadmaster:四边形也有同样的问题。如果我定义我的矩形让我改变宽度和高度,那么我假设我的四边形类让我重新定位四个顶点。如果我可以这样做,而且矩形和/或正方形是四边形,那么顶点重新定位方法将违反LSP。根本问题在于我们在小学几何中学到的层次结构是基于形状不可变的假设。在数学中拉伸矩形不会改变矩形——它会产生一个新的矩形(通常称为“图像”)。 - Laurence Gonsalves
啊,那个问题…… 实际上与面向对象编程无关。 问题与可变性有关。 如果您使用“不可变对象的OO”(完全可以),那么正方形/矩形“问题”就消失了。 要么你有一个不能被修改的对象(因为,比如说,苹果永远不会变成橙子),因此使用“使用不可变对象的OO”来实现这样的对象是明智的OOP选择,要么你有一个可以修改的对象,因此它永远不是“正方形”:它仍然是一个矩形,在某些状态下看起来像是正方形,但实际上并不是。 - SyntaxT3rr0r
5个回答

12

如果将正方形和矩形定义为不可变对象,那么就不会违反LSP原则。

问题在于,如果可以独立改变矩形的宽度和高度,而正方形是一种特殊的矩形,那么你就可以改变一个正方形使其不再是正方形。


4
是的,继承的重点在于派生类型应该添加功能,而不违反超类现有的任何功能。 - David R Tribble

1
我会说:不要。
正方形是矩形的一种特殊情况。因此,使用矩形。目前还不清楚是否有任何充分的理由来拥有一个独立的正方形类。
当然,这实际上取决于您对这些形状所做的操作。LSP是否得到满足取决于您对形状的操作。

如果一个函数只能处理边长相等的矩形,并接受“ImmutableSquare”类型的参数,那么就不需要在运行时检查长度和宽度是否相等。要将“ImmutableRectangle”传递给这样的函数,必须使用缩小类型转换;如果两边不相等,则类型转换可能会抛出异常,但这是缩小类型转换所期望的。由于给定非正方形矩形而导致函数本身抛出异常的危险是不存在的。 - supercat
@supercat,你肯定可以这样做并且它是有效的。但我的观点是,如果你有处理矩形的代码,那么很可能你不需要实现特殊的正方形代码。只需使用矩形版本即可。对于我所能想到的正方形所做的一切(绘制、面积、周长等),矩形版本都可以完成相同的工作。也许你有一个情况不是这样,对于这种情况,你的解决方案是很好的。 - Winston Ewert
在简单形状的特定情况下,这可能是真实的,但这个原则适用范围要广得多。例如,一个不可变的LinearTransfrom2d类可以表示任何2D线性变换,并且可以有一个派生的OrthogonalTransform2d类,它只表示xy和yx分量为零的变换。某些图形操作只能使用OrthogonalTransform2d进行,而其他操作则可以接受任何LinearTransform2d。 - supercat
@supercat,通常这种东西是可以工作的。一切取决于你对对象进行什么操作。 - Winston Ewert

0

你最好在构造函数中传递一个约束条件(例如bool square),将其存储为字段,并在计算“正方形”时检查字段(x、y、w、h?),从而加快这些计算的速度。

你可以使得如果设置了“square”约束,则设置宽度或高度会自动导致另一个匹配。 set(x, y, w, h) 方法始终有效,但如果参数不是正方形,则会抛出IllegalArgumentException或类似异常。


我认为我们在这里相当严重地违反了SRP原则。 - nick

0

克林顿说得最好:这取决于“is”的定义是什么

对于一个正方形来说是矩形的不可动摇的直觉来自我们的数学训练。数学对象是不可变和无标识的。如果你的程序确实是以数学意义上的正方形和矩形对象进行建模,那么正方形应该是矩形的子类型,并且它们应该是不可变的。任何适用于矩形的数学操作也适用于正方形。

然而,你的程序可能不是在建模数学对象。也许你正在建模图形屏幕对象。其中有数学方面,但还有更多。那么我们就陷入了困境。也许将矩形设计为正方形的子类型会更好,考虑到您想要对它们进行的所有操作。那么这完全违反了我们的数学直觉,我们不希望在设计中出现这种混淆。

这是一个可怕的事实:OOP是平庸的。你可能认为一些非常聪明的人进行了一些宏伟的研究,并提出了这个万能的编程模型。对于每一个难题,都有一个完美的解决方案,你不知道它是因为你还没有在理解这个神圣的启示方面变得足够出色。人们在OOP方面的争论比宗教敌人更有热情,他们相互扔大词汇和抽象概念,引用古老的文本中的原则和惯例,而这些文本实际上没有人能够真正理解。


0
如果它们是不可变的,如上所述,则不违反LSP:
public class Rectangle {
    int width;
    int height;

    public Rectangle(int w, int h) {
        width = w;
        height = h;
    }

    //getWidth(), getHeight, getArea(), etc, but no setters.
}


public Square extends Rectangle {
    public Square(int side) {
        super(side, side);
    }
}

现在,如果您有一个接受百分比的scale()方法,您可以以符合LSP的方式扩大矩形和正方形,但是在矩形上覆盖的接受两个边的grow()方法,当然会违反LSP,因为正方形需要正确地重写它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接