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


当前回答

使用指向基类的指针或引用的函数必须能够在不知道它的情况下使用派生类的对象。

当我第一次阅读LSP时,我认为这是一个非常严格的含义,本质上等同于接口实现和类型安全强制转换。这意味着语言本身要么保证LSP,要么不保证LSP。例如,在严格意义上,ThreeDBoard当然可以取代Board,就编译器而言。

在阅读了更多关于LSP的概念之后,我发现LSP的解释通常比这更广泛。

简而言之,对于客户端代码来说,“知道”指针后面的对象是派生类型而不是指针类型的含义并不仅限于类型安全。对LSP的遵守也可以通过探测对象的实际行为进行测试。也就是说,检查对象的状态和方法参数对方法调用结果或从对象抛出的异常类型的影响。

再次回到示例,理论上Board方法可以在ThreeDBoard上很好地工作。然而,在实践中,在不妨碍ThreeDBoard打算添加的功能的情况下,防止客户端可能无法正确处理的行为差异是非常困难的。

掌握了这些知识后,评估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。

设q(x)是关于类型为T的x的对象的可证明属性,那么q(y)对于类型为S的对象y应该是可证明的,其中S是T的子类型。


实际上,公认的答案并不是利斯科夫原理的反例。正方形自然是一个特定的矩形,因此从类矩形继承是完全有意义的。你只需要以这样的方式实现它:

@Override
public void setHeight(double height) {
   this.height = height;
   this.width = height; // since it's a square
}

@Override
public void setWidth(double width) {
   setHeight(width);
}

所以,提供了一个很好的例子,然而,这是一个反例:

class Family:
-- getChildrenCount()

class FamilyWithKids extends Family:
-- getChildrenCount() { return childrenCount; } // always > 0

class DeadFamilyWithKids extends FamilyWithKids:
-- getChildrenCount() { return 0; }
-- getChildrenCountWhenAlive() { return childrenCountWhenAlive; }

在这个实现中,DeadFamilyWithKids不能从FamilyWithKids继承,因为getChildrenCount()返回0,而从FamilyWithKids它应该总是返回大于0的值。

到目前为止,我发现LSP最清晰的解释是“利斯科夫替换原则说,派生类的对象应该能够替换基类的对象,而不会给系统带来任何错误,也不会修改基类的行为”。文中给出了违反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)

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

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

我在每个答案中都看到了矩形和正方形,以及如何违反LSP。

我想用一个真实的例子来展示LSP是如何符合的:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

这种设计符合LSP,因为无论我们选择使用哪种实现,行为都不会改变。

是的,你可以在这个配置中违反LSP,做一个简单的改变,像这样:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

现在子类型不能以同样的方式使用,因为它们不再产生相同的结果。