我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?


当前回答

利斯科夫替换原理

(固体)

继承子类型化

维基里斯科夫替换原理(LSP)

在子类型中不能加强先决条件。 后置条件不能在子类型中减弱。 超类型的不变量必须保留在子类型中。

子类型不应该要求调用者提供比超类型更多的(先决条件) 子类型不应该为小于超类型的调用者公开(后置条件)

*前置条件+后置条件=函数(方法)类型[Swift函数类型。Swift函数与方法

//Swift function
func foo(parameter: Class1) -> Class2

//function type
(Class1) -> Class2

//Precondition
Class1

//Postcondition
Class2

例子

//C3 -> C2 -> C1

class C1 {}
class C2: C1 {}
class C3: C2 {}

前提条件(如。函数参数类型)可以相同或更弱(力求-> C1) 后置条件(如。函数返回类型)可以相同或更强(力求-> C3) 超类型的不变变量[About]应该保持不变

斯威夫特

class A {
    func foo(a: C2) -> C2 {
        return C2()
    }
}

class B: A {
    override func foo(a: C1) -> C3 {
        return C3()
    }
}

Java

class A {
    public C2 foo(C2 a) {
        return new C2();
    }
}

class B extends A {
    @Override
    public C3 foo(C2 a) { //You are available pass only C2 as parameter
        return new C3();
    }
}

行为子类型化

维基里斯科夫替换原理(LSP)

子类型中方法参数类型的逆变性。子类型中方法返回类型的协方差。 子类型中的方法不能引发新的异常,除非它们是超类型的方法引发的异常的子类型。

[方差,协方差,逆变,不变性]

其他回答

Liskov替换原理(LSP, LSP)是面向对象编程中的一个概念,它指出:

函数使用指针或 基类的引用必须是 能够使用派生类的对象 在不知不觉中。

LSP的核心是关于接口和契约,以及如何决定何时扩展一个类,还是使用另一种策略(如组合)来实现您的目标。

我所见过的说明这一点的最有效的方法是《Head First OOA&D》。它们呈现的场景是,你是一名致力于为策略游戏构建框架的项目开发者。

他们展示了一个类,它代表一个板子,看起来像这样:

所有的方法都以X和Y坐标作为参数来定位tile在二维tile数组中的位置。这将允许游戏开发者在游戏过程中管理棋盘上的单位。

这本书继续改变了要求,说游戏框架工作也必须支持3D游戏板,以适应有飞行的游戏。因此引入了一个ThreeDBoard类,它扩展了Board。

乍一看,这似乎是个不错的决定。Board提供了高度和宽度属性,ThreeDBoard提供了Z轴。

当你看到从董事会继承的所有其他成员时,它就失效了。AddUnit, GetTile, GetUnits等方法在Board类中都采用X和Y参数,但ThreeDBoard也需要Z参数。

因此,您必须使用Z参数再次实现这些方法。Z参数没有Board类的上下文,从Board类继承的方法失去了意义。试图使用ThreeDBoard类作为其基类Board的代码单元将非常不走运。

也许我们应该另想办法。ThreeDBoard应该由Board对象组成,而不是扩展Board。Z轴上每单位一个板子对象。

这允许我们使用良好的面向对象原则,如封装和重用,并且不违反LSP。

一些补充:我想知道为什么没有人写基类的不变量、前提条件和后置条件,这些派生类必须遵守。 对于派生类D来说,基类B完全可转换,类D必须服从某些条件:

基类的内变体必须由派生类保留 派生类不能加强基类的先决条件 派生类不能削弱基类的后置条件。

因此派生类必须知道基类施加的上述三个条件。因此,子类型的规则是预先确定的。这意味着,只有当子类型遵守某些规则时,才应该遵守'IS A'关系。这些规则,以不变量、前置条件和后置条件的形式,应该由正式的“设计契约”来决定。

关于这个问题的进一步讨论可以在我的博客:利斯科夫替换原理

LSP的这种形式太强大了:

如果对于每个类型为S的对象o1,都有一个类型为T的对象o2,使得对于所有用T定义的程序P,当o1取代o2时,P的行为不变,那么S是T的子类型。

这基本上意味着S是t的另一个完全封装的实现,我可以大胆地认为性能是P行为的一部分……

因此,基本上,任何延迟绑定的使用都违反了LSP。当我们用一种类型的对象替换另一种类型的对象时,获得不同的行为是OO的全部意义所在!

维基百科引用的公式更好,因为属性取决于上下文,并不一定包括程序的整个行为。

利科夫替换原则指出,如果程序模块使用基类,则基类的引用可以被派生类替换,而不会影响程序模块的功能。

派生类型必须能够完全替代它们的基类型。

示例- java中的协变返回类型。

在一个非常简单的句子中,我们可以说:

子类不能违背它的基类特征。它必须有能力。我们可以说这和子类型是一样的。