我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
LSP关注不变量。
经典示例由以下伪代码声明给出(实现略):
class Rectangle {
int getHeight()
void setHeight(int value) {
postcondition: width didn’t change
}
int getWidth()
void setWidth(int value) {
postcondition: height didn’t change
}
}
class Square extends Rectangle { }
现在我们有一个问题,尽管接口匹配。原因是我们违反了源自正方形和矩形数学定义的不变量。getter和setter的工作方式,矩形应该满足以下不变量:
void invariant(Rectangle r) {
r.setHeight(200)
r.setWidth(100)
assert(r.getHeight() == 200 and r.getWidth() == 100)
}
然而,Square的正确实现必须违反这个不变量(以及显式后置条件),因此它不是Rectangle的有效替代品。
其他回答
可替代性是面向对象编程中的一个原则,它指出,在计算机程序中,如果S是T的子类型,那么类型T的对象可以被类型S的对象替换
让我们用Java做一个简单的例子:
不好的例子
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
鸭子能飞,因为它是鸟,但这个呢:
public class Ostrich extends Bird{}
鸵鸟是一种鸟,但它不能飞,鸵鸟类是鸟类的一个子类,但它不应该能够使用fly方法,这意味着我们打破了LSP原则。
很好的例子
public class Bird{}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
在一个非常简单的句子中,我们可以说:
子类不能违背它的基类特征。它必须有能力。我们可以说这和子类型是一样的。
该原则由Barbara Liskov在1987年提出,并通过关注超类及其子类型的行为来扩展开闭原则。
当我们考虑违反它的后果时,它的重要性就变得显而易见了。考虑一个使用以下类的应用程序。
public class Rectangle
{
private double width;
private double height;
public double Width
{
get
{
return width;
}
set
{
width = value;
}
}
public double Height
{
get
{
return height;
}
set
{
height = value;
}
}
}
想象一下,有一天,客户要求除了矩形之外还能操作正方形。因为正方形是矩形,所以square类应该派生自rectangle类。
public class Square : Rectangle
{
}
然而,这样做会遇到两个问题:
一个正方形不需要从矩形继承高度和宽度变量,如果我们必须创建成千上万个正方形对象,这可能会造成严重的内存浪费。 从矩形继承的width和height setter属性不适用于正方形,因为正方形的宽度和高度是相同的。 为了将height和width设置为相同的值,我们可以创建两个新属性,如下所示:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
现在,当有人设置一个正方形物体的宽度时,它的高度将相应地改变,反之亦然。
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
让我们继续考虑另一个函数:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
如果我们将一个方形对象的引用传递给这个函数,我们将违反LSP,因为该函数对其参数的导数不起作用。属性width和height不是多态的,因为它们在矩形中没有被声明为虚的(正方形对象将被损坏,因为高度不会被改变)。
然而,通过将setter属性声明为virtual,我们将面临另一个违反,即OCP。事实上,派生类正方形的创建会导致基类矩形的变化。
LSP的这种形式太强大了:
如果对于每个类型为S的对象o1,都有一个类型为T的对象o2,使得对于所有用T定义的程序P,当o1取代o2时,P的行为不变,那么S是T的子类型。
这基本上意味着S是t的另一个完全封装的实现,我可以大胆地认为性能是P行为的一部分……
因此,基本上,任何延迟绑定的使用都违反了LSP。当我们用一种类型的对象替换另一种类型的对象时,获得不同的行为是OO的全部意义所在!
维基百科引用的公式更好,因为属性取决于上下文,并不一定包括程序的整个行为。
利斯科夫替换原理
被重写的方法不应该保持为空 被重写的方法不应该抛出错误 基类或接口行为不应该因为派生类行为而进行修改(重做)。