ChatGPT解决这个技术问题 Extra ChatGPT

Liskov 替换原则的一个例子是什么?

我听说 Liskov 替换原则 (LSP) 是面向对象设计的基本原则。它是什么,有哪些使用示例?

更多 LSP 遵守和违规示例here
这是我发现的最好的例子之一:baeldung.com/java-liskov-substitution-principle
Liskov 替换原则指出子类应该可以盲目地替换它们的基类。
Liskov 替换原则指出子类应该可以盲目地替换它们的基类。
Liskov 替换原则指出子类应该可以盲目地替换它们的基类。

T
Todd

说明 LSP 的一个很好的例子(鲍勃叔叔在我最近听到的一个播客中给出)是有时在自然语言中听起来正确的东西在代码中并不完全有效。

在数学中,SquareRectangle。实际上,它是矩形的特化。 “is a”让你想用继承来建模。但是,如果在您使 Square 派生自 Rectangle 的代码中,则 Square 应该可以在您期望 Rectangle 的任何地方使用。这导致了一些奇怪的行为。

假设您的 Rectangle 基类上有 SetWidthSetHeight 方法;这似乎完全合乎逻辑。但是,如果您的 Rectangle 引用指向 Square,则 SetWidthSetHeight 没有意义,因为设置一个会更改另一个以匹配它。在这种情况下,Square 未能通过 Rectangle 的 Liskov 替换测试,并且让 SquareRectangle 继承的抽象是一个糟糕的抽象。

https://i.stack.imgur.com/ilxzO.jpg

你们都应该看看另一个无价的SOLID Principles Explained With Motivational Posters


@m-sharp 如果它是一个不可变的矩形,而不是 SetWidth 和 SetHeight,我们有方法 GetWidth 和 GetHeight 怎么办?
故事的寓意:根据行为而不是属性对类进行建模;根据属性而不是行为对数据进行建模。如果它的行为像一只鸭子,那它肯定是一只鸟。
好吧,正方形显然是现实世界中的一种矩形。我们是否可以在代码中对此进行建模取决于规范。 LSP 指出的是子类型行为应该与基本类型规范中定义的基本类型行为相匹配。如果矩形基本类型规范说高度和宽度可以独立设置,那么 LSP 说正方形不能是矩形的子类型。如果矩形规范说矩形是不可变的,那么正方形可以是矩形的子类型。这都是关于维护为基本类型指定的行为的子类型。
@Pacerier 如果它是不可变的,则没有问题。这里真正的问题是我们不是在为矩形建模,而是“可变形矩形”,即在创建后可以修改宽度或高度的矩形(我们仍然认为它是同一个对象)。如果我们以这种方式查看矩形类,很明显正方形不是“可变形的矩形”,因为正方形不能被重新塑造并且仍然是正方形(通常)。在数学上,我们没有看到问题,因为可变性在数学环境中甚至没有意义。
我有一个关于原理的问题。如果 Square.setWidth(int width) 是这样实现的,为什么会出现问题:this.width = width; this.height = width;?在这种情况下,可以保证宽度等于高度。
C
Community

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

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

LSP 的核心是关于接口和契约,以及如何决定何时扩展类与使用组合等其他策略来实现目标。

我见过的最有效的说明这一点的方法是在 Head First OOA&D 中。他们提出了一个场景,您是一个项目的开发人员,为策略游戏构建框架。

他们提供了一个类,该类代表一个如下所示的板:

https://oncodebynotmyself.files.wordpress.com/2011/03/board_thumb.png

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

该书继续更改要求,说游戏框架还必须支持 3D 游戏板以适应具有飞行的游戏。因此引入了扩展 BoardThreeDBoard 类。

乍一看,这似乎是一个不错的决定。 Board 提供 HeightWidth 属性,ThreeDBoard 提供 Z 轴。

当您查看从 Board 继承的所有其他成员时,它就会崩溃。 AddUnitGetTileGetUnits 等的方法都采用 Board 类中的 X 和 Y 参数,但 ThreeDBoard 也需要 Z 参数。

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

也许我们应该找到另一种方法。 ThreeDBoard 应该由 Board 对象组成,而不是扩展 Board。 Z 轴每单位一个 Board 对象。

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


另请参阅 Wikipedia 上的 Circle-Ellipse Problem,了解类似但更简单的示例。
重新引用@NotMySelf:“我认为这个例子只是为了证明在 ThreeDBoard 的上下文中从 board 继承没有意义,并且所有方法签名对于 Z 轴都没有意义。”。
因此,如果我们向 Child 类添加另一个方法,但 Parent 的所有功能在 Child 类中仍然有意义,那会破坏 LSP 吗?因为一方面我们修改了使用 Child 的接口,另一方面如果我们将 Child 转换为 Parent,则期望 Parent 的代码可以正常工作。
这是一个反 Liskov 的例子。 Liskov 让我们从 Square 中推导出 Rectangle。更多参数类来自更少参数类。你已经很好地表明它是坏的。将其标记为答案并在 liskov 问题的反 liskov 答案中被投票 200 次,这真是一个很好的笑话。里氏原理真的是谬误吗?
我已经看到继承以错误的方式工作。这是一个例子。基类应该是 3DBoard 和派生类 Board。 Board 仍然有一个 Z 轴 Max(Z) = Min(Z) = 1
m
maysara

可替换性是面向对象编程中的一个原则,它指出,在计算机程序中,如果 S 是 T 的子类型,则 T 类型的对象可以被 S 类型的对象替换

让我们用Java做一个简单的例子:

不好的例子

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

鸭子可以飞,因为它是一只鸟,但是这个呢:

public class Ostrich extends Bird{}

Ostrich 是鸟,但它不会飞,Ostrich 类是 Bird 类的子类型,但它应该不能使用 fly 方法,这意味着我们违反了 LSP 原则。

好的例子

public class Bird{}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

很好的例子,但是如果客户有 Bird bird,你会怎么做。您必须将对象投射到 FlyingBirds 才能使用 fly,这不是很好吗?
不可以。如果客户端有 Bird bird,这意味着它不能使用 fly()。而已。传递 Duck 不会改变这一事实。如果客户端有 FlyingBirds bird,那么即使它通过了 Duck,它也应该始终以相同的方式工作。
这难道不是接口隔离的一个很好的例子吗?
很好的例子谢谢男人
使用“Flyable”接口怎么样(想不出更好的名字)。这样我们就不会陷入这种僵化的等级制度。除非我们知道真的需要它。
K
Konrad Rudolph

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 的工作方式,Rectangle 应满足以下不变量:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

但是,Square 的正确实现必须违反此不变量(以及显式后置条件),因此它不是 Rectangle 的有效替代品。


因此,很难使用“OO”来建模我们可能想要实际建模的任何东西。
@DrPizza:当然。然而,有两件事。首先,这种关系仍然可以在 OOP 中建模,尽管不完整或使用更复杂的弯路(选择适合您问题的方式)。其次,没有更好的选择。其他映射/建模具有相同或相似的问题。 ;-)
@NickW 在某些情况下(但不是在上面),您可以简单地反转继承链——从逻辑上讲,一个 2D 点是一个 3D 点,其中第三维被忽略(或 0——所有点都位于同一平面上) 3D 空间)。但这当然不是很实用。一般来说,这是继承没有真正帮助的情况之一,实体之间不存在自然关系。分别为它们建模(至少我不知道更好的方法)。
OOP 旨在对行为而非数据进行建模。您的类甚至在违反 LSP 之前就违反了封装。
@AustinWBryan 是的;我在这个领域工作的时间越长,我就越倾向于只对接口和抽象基类使用继承,而对其余部分使用组合。有时需要做更多的工作(明智地打字),但它避免了一大堆问题,并且得到了其他有经验的程序员的广泛响应。
s
shA.t

罗伯特马丁有一个优秀的paper on the Liskov Substitution Principle。它讨论了可能违反该原则的微妙和不那么微妙的方式。

论文的一些相关部分(请注意,第二个示例非常精简):

违反 LSP 的简单示例 违反此原则的最明显的违反之一是使用 C++ 运行时类型信息 (RTTI) 根据对象的类型选择函数。即: void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast(s)); } 显然,DrawShape 函数的格式不正确。它必须知道 Shape 类的每个可能的派生类,并且每当创建 Shape 的新派生类时都必须更改它。事实上,许多人将此功能的结构视为对面向对象设计的诅咒。方形和矩形,更微妙的违反。但是,还有其他更微妙的方式来违反 LSP。考虑一个使用 Rectangle 类的应用程序,如下所述: class Rectangle { public: void SetWidth(double w) {itsWidth=w;} void SetHeight(double h) {itsHeight=w;} double GetHeight() const {return itsHeight; } double GetWidth() const {return itsWidth;} private: double itsWidth;将其高度加倍; }; [...] 想象有一天,除了矩形之外,用户还需要操作正方形的能力。 [...] 显然,正方形是所有正常意图和目的的矩形。由于 ISA 关系成立,因此将 Square 类建模为从 Rectangle 派生是合乎逻辑的。 [...] Square 将继承 SetWidth 和 SetHeight 函数。这些函数完全不适合正方形,因为正方形的宽度和高度是相同的。这应该是设计存在问题的重要线索。但是,有一种方法可以回避这个问题。我们可以覆盖 SetWidth 和 SetHeight [...] 但考虑以下函数: void f(Rectangle& r) { r.SetWidth(32); // 调用 Rectangle::SetWidth } 如果我们将一个 Square 对象的引用传递给这个函数,这个 Square 对象将被破坏,因为高度不会改变。这明显违反了 LSP。该函数不适用于其参数的导数。 [...]


很晚了,但我认为这是该论文中的一个有趣的引述:Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. 如果子类先决条件比父类先决条件强,则不能在不违反先决条件的情况下用子类替换父类-健康)状况。因此,LSP。
C
Community

LSP 在某些代码认为它正在调用类型 T 的方法时是必需的,并且可能会在不知不觉中调用类型 S 的方法,其中 S extends T(即 S 继承、派生或是子类型的,超类型T)。

例如,当使用类型为 S 的参数值调用(即调用)具有类型 T 的输入参数的函数时,就会发生这种情况。或者,如果类型为 T 的标识符被分配一个类型为 S 的值。

val id : T = new S() // id thinks it's a T, but is a S

LSP 要求 T 类型的方法(例如 Rectangle)的期望(即不变量),而不是在调用 S 类型的方法(例如 Square)时不违反。

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

即使具有不可变字段的类型仍然具有不变量,例如,不可变的 Rectangle 设置器期望维度被独立修改,但不可变的 Square 设置器违反了这种期望。

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP 要求子类型 S 的每个方法必须具有逆变输入参数和协变输出。

逆变是指方差与继承的方向相反,即类型Si,子类型S的每个方法的每个输入参数,必须相同或者是一个超类型超类型 T 的相应方法的相应输入参数的类型 Ti

协方差是指方差在继承的同一方向,即类型So,子类型S的每个方法的输出,必须相同或属于该类型的子类型超类型 T 的相应方法的相应输出的 To

这是因为如果调用者认为它具有 T 类型,认为它正在调用 T 的方法,那么它提供 Ti 类型的参数并将输出分配给 To 类型。当它实际调用 S 的对应方法时,则将每个 Ti 输入参数分配给一个 Si 输入参数,并将 So 输出分配给类型 To。因此,如果 SiTi 不是逆变的,则子类型 Xi(它不会是 Si 的子类型)可以分配给 Ti

此外,对于在类型多态性参数(即泛型)上具有定义站点方差注释的语言(例如 Scala 或 Ceylon),类型 T 的每个类型参数的方差注释的同向或相反方向必须是 { 1} 或相同方向分别指向具有类型参数类型的每个输入参数或输出(T 的每个方法)。

此外,对于具有函数类型的每个输入参数或输出,所需的方差方向是相反的。此规则以递归方式应用。

Subtyping is appropriate 可以枚举不变量。

关于如何对不变量进行建模的研究正在进行中,以便它们由编译器强制执行。

Typestate(参见第 3 页)声明并强制执行与类型正交的状态不变量。或者,可以通过 converting assertions to types 强制执行不变量。例如,要断言文件在关闭之前已打开,则 File.open() 可以返回 OpenFile 类型,该类型包含 File 中不可用的 close() 方法。 tic-tac-toe API 可以是在编译时使用类型来强制执行不变量的另一个示例。类型系统甚至可以是图灵完备的,例如Scala。依赖类型语言和定理证明形式化了高阶类型的模型。

由于abstract over extension需要语义,我希望使用类型来建模不变量,即统一的高阶指称语义,优于Typestate。 “扩展”是指不协调的、模块化的开发的无限的、置换的组合。因为在我看来,统一和自由度的对立面,有两个相互依赖的模型(例如类型和类型状态)来表达共享语义,它们不能相互统一以实现可扩展的组合.例如,类似 Expression Problem 的扩展在子类型、函数重载和参数类型域中得到统一。

我的理论立场是,对于 knowledge to exist(请参阅“集中化是盲目且不合适的”部分),永远不会有一个通用模型可以强制 100% 覆盖图灵完备中的所有可能不变量计算机语言。知识要存在,就存在很多意想不到的可能性,即无序和熵必须总是在增加。这就是熵力。证明潜在扩展的所有可能计算,就是先验地计算所有可能的扩展。

这就是Halting Theorem 存在的原因,也就是说,在图灵完备的编程语言中是否每个可能的程序都终止是不可判定的。可以证明某些特定程序终止(已定义和计算了所有可能性的程序)。但是不可能证明该程序的所有可能扩展都终止,除非该程序扩展的可能性不是图灵完备的(例如,通过依赖类型)。由于图灵完备性的基本要求是unbounded recursion,因此可以直观地理解哥德尔不完备性定理和罗素悖论如何应用于扩展。

对这些定理的解释将它们纳入对熵力的广义概念理解中:

哥德尔不完备定理:任何可以证明所有算术真理的形式理论都是不一致的。

罗素悖论:可以包含集合的集合的每个成员规则,要么枚举每个成员的特定类型,要么包含自身。因此,集合要么不能扩展,要么是无限递归。例如,所有不是茶壶的东西的集合,包括它自己,它包括它自己,它包括它自己,等等……。因此,如果规则(可能包含一个集合并且)不枚举特定类型(即允许所有未指定的类型)并且不允许无界扩展,则该规则是不一致的。这是不属于自身成员的集合。这种无法在所有可能的扩展上保持一致和完全枚举的能力,就是哥德尔的不完备性定理。

Liskov Substition Principle:一般来说,一个集合是否是另一个集合的子集是一个不可判定的问题,即继承一般是不可判定的。

林斯基参考:当事物被描述或感知时,它的计算是不确定的,即感知(现实)没有绝对的参考点。

科斯定理:没有外部参考点,因此任何无界外部可能性的障碍都会失效。

热力学第二定律:整个宇宙(一个封闭系统,即万物)趋向于最大无序,即最大独立可能性。


@Shelyby:你混合了太多东西。事情并没有你说的那么混乱。您的许多理论断言都建立在站不住脚的基础上,例如“为了存在知识,存在很多意想不到的可能性,......”并且“通常,任何集合是否是另一个集合的子集是一个无法确定的问题,即继承通常是不可判定的'。您可以为这些要点中的每一个创建一个单独的博客。无论如何,你的断言和假设是非常值得怀疑的。不可以使用自己不知道的东西!
@aknon I have a blog 更深入地解释了这些问题。我的无限时空 TOE 模型是无限频率。递归归纳函数具有已知的起始值和无限的结束界限,或者协归纳函数具有未知的结束值和已知的起始界限,这对我来说并不令人困惑。一旦引入递归,相对论就会出现问题。这就是为什么Turing complete is equivalent to unbounded recursion
@ShelbyMooreIII 你的方向太多了。这不是一个答案。
@Soldalma 这是一个答案。你没有在答案部分看到它。你的是评论,因为它在评论部分。
就像你与 scala 世界的混合!
S
Steve Chamaillard

我在每个答案中都看到了矩形和正方形,以及如何违反 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 !
    }
}

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


只要我们将 Database::selectQuery 的语义限制为仅支持 all 数据库引擎支持的 SQL 子集,该示例就不会违反 LSP。这几乎不实用......也就是说,这个例子仍然比这里使用的大多数其他例子更容易掌握。
我发现这个答案是最容易掌握的。
在数据库上应用 LSP 实用吗?我看到大多数(如果不是全部)数据库操作都需要包装,并且容易出错。尽管好的一面是 API 保持不变,即使它是 SQL 与 NoSQL。
Y
Yogesh Umesh Vaity

有一个清单可以确定您是否违反了 Liskov。

如果您违反以下一项 -> 您违反了 Liskov。

如果您不违反任何-> 无法得出任何结论。

检查清单:

派生类中不应抛出新异常:如果您的基类抛出 ArgumentNullException,那么您的子类只允许抛出 ArgumentNullException 类型的异常或从 ArgumentNullException 派生的任何异常。抛出 IndexOutOfRangeException 违反了 Liskov。

前提条件无法加强:假设您的基类与成员 int 一起使用。现在您的子类型要求该 int 为正数。这是强化的先决条件,现在任何以前使用负整数都可以正常工作的代码都被破坏了。

不能削弱后置条件:假设您的基类要求在方法返回之前关闭所有与数据库的连接。在您的子类中,您覆盖了该方法并保持连接打开以供进一步重用。您削弱了该方法的后置条件。

必须保留不变量:要实现的最困难和最痛苦的约束。不变量有时隐藏在基类中,显示它们的唯一方法是阅读基类的代码。基本上,您必须确保在覆盖方法时,任何不可更改的内容在执行覆盖的方法后必须保持不变。我能想到的最好的事情是在基类中强制执行这些不变的约束,但这并不容易。

历史约束:重写方法时,不允许修改基类中不可修改的属性。查看这些代码,您可以看到 Name 被定义为不可修改(私有集),但 SubType 引入了允许修改它的新方法(通过反射): public class SuperType { public string Name { get;私人套装; } public SuperType(string name, int age) { Name = name;年龄=年龄; } } 公共类子类型: SuperType { public void ChangeName(string newName) { var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName); } }

还有其他 2 项:方法参数的逆变和返回类型的协方差。但这在 C# 中是不可能的(我是 C# 开发人员),所以我不关心它们。


我也是一名 C# 开发人员,我会告诉你最后的陈述在 Visual Studio 2010 中是不正确的,带有 .Net 4.0 框架。返回类型的协变允许比接口定义的更派生的返回类型。示例:示例:IEnumerable<T> (T 是协变的) IEnumerator<T> (T 是协变的) IQueryable<T> (T 是协变的) IGrouping<TKey, TElement> (TKey 和 TElement 是协变的) IComparer<T> (T 是逆变的) IEqualityComparer<T> (T 是逆变的) IComparable<T> (T 是逆变的)msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
P
Pang

长话短说,让我们留下矩形矩形和正方形正方形,扩展父类时的实际示例,您必须保留确切的父 API 或扩展它。

假设您有一个基础 ItemsRepository。

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

还有一个扩展它的子类:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

然后,您可以让客户端使用 Base ItemsRepository API 并依赖它。

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

当用子类替换父类时,LSP 会破坏 API 的合同。

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

您可以在我的课程中了解有关编写可维护软件的更多信息:https://www.udemy.com/enterprise-php/


这是一个更好的例子。谢谢!
K
Khaled Qasem

让我们用Java来说明:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

这里没有问题,对吧?汽车绝对是一种交通工具,在这里我们可以看到它重写了其超类的 startEngine() 方法。

让我们添加另一个运输设备:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

现在一切都没有按计划进行!是的,自行车是一种交通工具,但是它没有引擎,因此无法实现 startEngine() 方法。

这些是违反里氏替换原则所导致的问题,通常可以通过什么都不做的方法来识别,甚至无法实现。

这些问题的解决方案是正确的继承层次结构,在我们的例子中,我们将通过区分带和不带引擎的运输设备类别来解决问题。尽管自行车是一种交通工具,但它没有引擎。在这个例子中,我们对运输设备的定义是错误的。它不应该有引擎。

我们可以重构我们的 TransportationDevice 类如下:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

现在我们可以为非机动设备扩展TransportationDevice。

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

并为机动设备扩展TransportationDevice。这里更适合添加Engine对象。

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

因此,我们的 Car 类变得更加专业,同时遵守 Liskov 替换原则。

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

而且我们的 Bicycle 类也符合 Liskov 替换原则。

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

C
Charlie Martin

LSP 是关于类契约的规则:如果基类满足契约,那么 LSP 的派生类也必须满足该契约。

在伪python中

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

如果每次在 Derived 对象上调用 Foo 时,只要 arg 相同,它就满足 LSP 与在 Base 对象上调用 Foo 完全相同的结果。


但是......如果你总是得到相同的行为,那么派生类的意义何在?
您错过了一点:这是观察到的相同行为。例如,您可以用功能等效但具有 O(lg n) 性能的东西替换具有 O(n) 性能的东西。或者,您可以替换使用 MySQL 实现的访问数据的东西,并将其替换为内存数据库。
@Charlie Martin,对接口而不是实现进行编码-我很喜欢。这不是 OOP 独有的; Clojure 等函数式语言也促进了这一点。即使在 Java 或 C# 方面,我认为使用接口而不是使用抽象类加上类层次结构对于您提供的示例来说是很自然的。 Python 不是强类型的,也没有真正的接口,至少没有明确的接口。我的困难是我做了几年OOP,没有坚持SOLID。现在我遇到了它,它似乎是有限的,几乎是自相矛盾的。
好吧,你需要回去看看芭芭拉的原始论文。 reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps 它并没有真正用接口来说明,它是一种逻辑关系,在任何具有某种继承形式的编程语言中都成立(或不成立)。
@HamishGrubijan 我不知道谁告诉你 Python 不是强类型的,但他们在骗你(如果你不相信我,启动 Python 解释器并尝试 2 + "2")。也许您将“强类型”与“静态类型”混淆了?
s
snagpaul

我想每个人都在技术上涵盖了 LSP 是什么:您基本上希望能够从子类型细节中抽象出来并安全地使用超类型。

所以 Liskov 有 3 个基本规则:

签名规则:在语法上,子类型中父类型的每个操作都应该有一个有效的实现。编译器将能够为您检查的东西。关于抛出更少的异常并至少与超类型方法一样可访问,有一个小规则。方法规则:这些操作的实现在语义上是合理的。较弱的前提条件:子类型函数应该至少采用超类型作为输入的内容,如果不是更多的话。更强的后置条件:它们应该产生超类型方法产生的输出的一个子集。属性规则:这超出了单个函数调用。不变量:总是真实的事物必须保持真实。例如。 Set 的大小永远不会是负数。进化属性:通常与不变性或对象可以处于的状态类型有关。或者对象只会增长而不会缩小,因此子类型方法不应该这样做。

所有这些属性都需要保留,并且额外的子类型功能不应违反超类型属性。

如果这三件事都得到了照顾,那么您就已经从底层的东西中抽象出来了,并且您正在编写松散耦合的代码。

资料来源:Java 程序开发 - Barbara Liskov


P
Peter Mortensen

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

当我第一次阅读有关 LSP 的内容时,我认为这是非常严格的意思,本质上将其等同于接口实现和类型安全的强制转换。这意味着语言本身可以保证或不保证 LSP。例如,在这个严格意义上,就编译器而言,ThreeDBoard 肯定可以替代 Board。

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

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

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

掌握了这些知识,评估 LSP 的依从性可以成为一个很好的工具,用于确定何时组合是扩展现有功能而不是继承更合适的机制。


C
Community

使用 LSP 的一个重要例子是在软件测试中。

如果我有一个类 A 是 B 的符合 LSP 的子类,那么我可以重用 B 的测试套件来测试 A。

要完全测试子类 A,我可能需要添加更多测试用例,但至少我可以重用所有超类 B 的测试用例。

一种实现方式是通过构建 McGregor 所说的“用于测试的并行层次结构”:我的 ATest 类将从 BTest 继承。然后需要某种形式的注入来确保测试用例与 A 类型的对象而不是 B 类型的对象一起工作(一个简单的模板方法模式就可以了)。

请注意,为所有子类实现重用超级测试套件实际上是一种测试这些子类实现是否符合 LSP 的方法。因此,人们也可以争辩说应该在任何子类的上下文中运行超类测试套件。

另请参阅 Stackoverflow 问题“Can I implement a series of reusable tests to test an interface's implementation?”的答案


R
Rahamath

里氏替换原则

被覆盖的方法不应为空

被覆盖的方法不应该抛出错误

基类或接口行为不应因为派生类行为而进行修改(返工)。


D
Damien Pollet

LSP 的这个公式太强了:

如果对于每个 S 类型的对象 o1 都有一个 T 类型的对象 o2 使得对于所有以 T 定义的程序 P,当 o1 替换 o2 时 P 的行为不变,则 S 是 T 的子类型。

这基本上意味着 S 是与 T 完全相同的东西的另一个完全封装的实现。我可以大胆地决定性能是 P 行为的一部分...

因此,基本上,任何后期绑定的使用都违反了 LSP。当我们用一种对象替换另一种对象时,获得不同行为是 OO 的全部要点!

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


嗯,这个提法是芭芭拉·利斯科夫自己的。 Barbara Liskov,“Data Abstraction and Hierarchy”,SIGPLAN Notices,23,5(1988 年 5 月)。它不是“太强”,它是“完全正确”,它没有你认为的那种暗示。它很强大,但力量恰到好处。
然后,现实生活中的亚型很少:)
“行为不变”并不意味着子类型会给您完全相同的具体结果值。这意味着子类型的行为与基类型中的预期相匹配。示例:基本类型 Shape 可以有一个 draw() 方法并规定该方法应该呈现形状。 Shape 的两个子类型(例如 Square 和 Circle)都将实现 draw() 方法,结果看起来会有所不同。但只要行为(渲染形状)与 Shape 的指定行为相匹配,那么 Square 和 Circle 将是符合 LSP 的 Shape 的子类型。
d
dlmeetei

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

子类不得违反其基类特征。它必须有能力。我们可以说它与子类型相同。


G
GauRang Omar

Liskov 的替换原则(LSP) 我们一直在设计程序模块并创建一些类层次结构。然后我们扩展一些类,创建一些派生类。我们必须确保新的派生类只是扩展而不替换旧类的功能。否则,新类在现有程序模块中使用时会产生不良影响。 Liskov 的替换原则指出,如果程序模块正在使用 Base 类,那么对 Base 类的引用可以替换为 Derived 类,而不会影响程序模块的功能。

例子:

以下是违反 Liskov 替换原则的经典示例。在示例中,使用了 2 个类:Rectangle 和 Square。假设在应用程序的某处使用了 Rectangle 对象。我们扩展应用程序并添加 Square 类。 square 类由工厂模式返回,基于某些条件,我们不知道将返回的确切对象类型。但我们知道它是一个矩形。我们得到矩形对象,将宽度设置为 5,高度设置为 10,然后得到面积。对于宽度为 5 和高度为 10 的矩形,面积应为 50。相反,结果将为 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

结论:这个原则只是 Open Close 原则的扩展,它意味着我们必须确保新的派生类在不改变其行为的情况下扩展基类。

另请参阅:Open Close Principle

一些类似的概念以获得更好的结构:Convention over configuration


j
johannesMatevosyan

The LSP in simple terms 指出同一个超类的对象应该能够相互交换而不会破坏任何东西。

例如,如果我们有一个 Cat 和一个从 Animal 类派生的 Dog 类,则任何使用 Animal 类的函数都应该能够使用 CatDog 并正常运行。


I
Ivan Porta

该原则由 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; 
    } 
  } 
}

想象有一天,客户要求除了矩形之外还需要操作正方形的能力。既然正方形就是长方形,那么正方形类应该派生自Rectangle类。

public class Square : Rectangle
{
} 

但是,这样做我们会遇到两个问题:

正方形不需要从矩形继承的高度和宽度变量,如果我们必须创建数十万个正方形对象,这可能会在内存中造成重大浪费。从矩形继承的宽度和高度设置器属性不适用于正方形,因为正方形的宽度和高度是相同的。为了将高度和宽度设置为相同的值,我们可以创建两个新属性,如下所示:

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,因为该函数不适用于其参数的导数。属性宽度和高度不是多态的,因为它们没有在矩形中声明为虚拟(方形对象将被破坏,因为高度不会改变)。

但是,通过将 setter 属性声明为虚拟,我们将面临另一个违规行为,即 OCP。事实上,派生类正方形的创建导致基类矩形的变化。


在方法 A() 中,我认为您的意思是 r.Width = 32;,因为 Rectangle 没有 SetWidth() 方法。
a
aknon

一些附录:我想知道为什么没有人写关于派生类必须遵守的基类的 Invariant 、前置条件和后置条件。对于派生类 D 完全可以被基类 B 替代,类 D 必须遵守某些条件:

基类的变量必须由派生类保留

派生类不能强化基类的前置条件

派生类不能削弱基类的后置条件。

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

可在我的博客上对此进行进一步讨论:Liskov Substitution principle


W
Wouter

正方形是宽度等于高度的矩形。如果正方形为宽度和高度设置了两种不同的大小,则它违反了正方形不变量。这是通过引入副作用来解决的。但是如果矩形有一个 setSize(height, width),前提是 0 < height 和 0 < width。派生的子类型方法需要 height == width;一个更强的前提条件(这违反了 lsp)。这表明虽然正方形是一个矩形,但它不是一个有效的子类型,因为前提条件被加强了。解决方法(通常是一件坏事)会导致副作用,这会削弱后置条件(违反 lsp)。基础上的 setWidth 具有后置条件 0 < 宽度。派生用高度==宽度削弱它。

因此,可调整大小的正方形不是可调整大小的矩形。


S
Sarmad Sohail

它指出如果 C 是 E 的子类型,则可以用 C 类型的对象替换 E,而不会改变或破坏程序的行为。简而言之,派生类应该可以替代它们的父类。例如,如果农民的儿子是农民,那么他可以代替父亲工作,但如果农民的儿子是板球运动员,那么他不能代替父亲工作。

违规示例:

public class Plane{

  public void startEngine(){}      

}        
public class FighterJet extends Plane{}
    
public class PaperPlane extends Plane{}

在给定示例中,FighterPlanePaperPlane 类都扩展了包含 startEngine() 方法的 Plane 类。所以很明显,FighterPlane 可以启动引擎,但 PaperPlane 不能,所以它破坏了 LSP

PaperPlane 类虽然扩展了 Plane 类并且应该可以替代它,但它不是 Plane 实例可以替换的合格实体,因为纸飞机无法启动引擎,因为它没有引擎。所以一个很好的例子是,

尊敬的例子:

public class Plane{ 
} 
public class RealPlane{

  public void startEngine(){} 

}
public class FighterJet extends RealPlane{} 
public class PaperPlane extends Plane{}

本质上,另一种说法是:驻留在超类中的所有方法都需要适当地应用于其所有子类。然而,这只是替代原则的一个标准。
j
jferard

大图:

什么是里氏替换原则?它是关于什么是(什么不是)给定类型的子类型。

为什么如此重要?因为子类型和子类之间是有区别的。

例子

与其他答案不同,我不会从违反 Liskov 替换原则 (LSP) 开始,而是从 LSP 合规开始。我使用 Java,但在每种 OOP 语言中几乎都是一样的。

圆形和彩色圆形

几何示例在这里似乎很受欢迎。

class Circle {
    private int radius;

    public Circle(int radius) {
        if (radius < 0) {
            throw new RuntimeException("Radius should be >= 0");
        }
        this.radius = radius;
    }

    public int getRadius() {
        return this.radius;
    }
}

半径不允许为负数。这是一个子类:

class ColoredCircle extends Circle {
    private Color color; // defined elsewhere

    public ColoredCircle(int radius, Color color) {
        super(radius);
        this.color = color;
    }

    public Color getColor() {
        return this.color;
    }
}

根据 LSP,这个子类是 Circle 的子类型。

LSP 指出:

如果对于每个 S 类型的对象 o1 都有一个 T 类型的对象 o2 使得对于所有以 T 定义的程序 P,当 o1 替换 o2 时 P 的行为不变,则 S 是 T 的子类型。 Barbara Liskov,“数据抽象和层次结构”,SIGPLAN Notices,23,5(1988 年 5 月))

这里,对于每个 ColoredCircle 实例 o1,考虑具有相同半径 o2Circle 实例。对于每个使用 Circle 对象的程序,如果将 o2 替换为 o1,则任何使用 Circle 的程序的行为在替换后都将保持不变。 (请注意,这是理论上的:使用 ColoredCircle 实例比使用 Circle 实例更快地耗尽内存,但这与此处无关。)

我们如何根据 o1 找到 o2 ?我们只是去掉 color 属性并保留 radius 属性。我将转换称为 o1 -> o2 来自 Circle 空间的 CircleColor 空间的投影

反例

让我们创建另一个示例来说明违反 LSP 的情况。

圆形和方形

想象一下前一个 Circle 类的这个子类:

class Square extends Circle {
    private int sideSize;

    public Square(int sideSize) {
        super(0);
        this.sideSize = sideSize;
    }

    @Override
    public int getRadius() {
        return -1; // I'm a square, I don't care
    }

    public int getSideSize() {
        return this.sideSize;
    }
}

违反 LSP

现在,看看这个程序:

public class Liskov {
    public static void program(Circle c) {
        System.out.println("The radius is "+c.getRadius());
    }

我们使用 Circle 对象和 Square 对象测试程序。

    public static void main(String [] args){
        Liskov.program(new Circle(2)); // prints "The radius is 2"
        Liskov.program(new Square(2)); // prints "The radius is -1"
    }
}

发生了什么 ?直观地说,虽然 SquareCircle 的子类,但 Square 不是 Circle 的子类型,因为没有常规 Circle 实例的半径永远不会是 -1。

形式上,这违反了里氏替换原则。

我们有一个根据 Circle 定义的程序,并且没有 Circle 对象可以替换该程序中的 new Square(2)(或任何 Square 实例)并保持行为不变:记住任何Circle 总是积极的。

子类和子类型

现在我们知道为什么子类并不总是子类型。当子类不是子类型时,即存在 LSP 违规时,某些程序(至少一个)的行为将不会始终是预期的行为。这非常令人沮丧,通常被解释为错误。

在理想世界中,编译器或解释器将能够检查给定的子类是否是真正的子类型,但我们不在理想世界中。

静态类型

如果有一些静态类型,你会在编译时受到超类签名的约束。 Square.getRadius() 不能返回 StringList

如果没有静态类型,如果一个参数的类型错误(除非类型很弱)或参数的数量不一致(除非语言非常宽松),您将在运行时收到错误。

关于静态类型的注意事项:存在返回类型的协变(S 的方法可以返回 T 的相同方法的返回类型的子类)和参数类型的逆变(S 的方法可以接受T 的相同方法的相同参数的参数的超类)。这是下面解释的前置条件和后置条件的特定情况。

合同设计

还有更多。某些语言(我认为是 Eiffel)提供了一种强制遵守 LSP 的机制。

更不用说初始对象 o1 的投影 o2 的确定了,如果 对于任何参数 x 和任何方法,如果用 o1 代替 o2,我们可以预期任何程序的相同行为f

如果 o2.f(x) 是一个有效的调用,那么 o1.f(x) 也应该是一个有效的调用 (1)。

o1.f(x) 的结果(返回值、控制台显示等)应该等于 o2.f(x) 的结果,或者至少同样有效 (2)。

o1.f(x) 应该让 o1 处于内部状态,o2.f(x) 应该让 o2 处于内部状态,以便下一个函数调用将确保 (1)、(2) 和 (3) 仍然有效(3)。

请注意,如果函数 f 是纯函数,则 (3) 是免费提供的。这就是我们喜欢拥有不可变对象的原因。

这些条件是关于类的语义(期望什么),而不仅仅是类的语法。而且,这些条件非常强。但是它们可以通过合同编程的设计断言来近似。这些断言是一种确保类型语义得到维护的方法。违反合同会导致运行时错误。

前置条件定义什么是有效调用。当对一个类进行子分类时,前提条件可能只会被削弱(Sf 接受多于 Tf)(a)。

后置条件定义什么是有效结果。当对一个类进行子类化时,后置条件可能只会被加强(Sf 提供的比 Tf 多)(b)。

不变量定义了什么是有效的内部状态。当对一个类进行子类化时,不变量必须保持不变 (c)。

我们看到,粗略地说,(a)确保(1)和(b)确保(2),但(c)比(3)弱。此外,断言有时难以表达。

想象一个类 Counter 具有返回下一个整数的唯一方法 Counter.counter()。您如何为此编写后置条件?想象一个类 Random 有一个方法 Random.gaussian(),它返回一个介于 0.0 和 1.0 之间的浮点数。您如何编写后置条件来检查分布是否为高斯分布?这可能是可能的,但成本会很高,以至于我们将依赖测试而不是后置条件。

结论

不幸的是,子类并不总是子类型。这可能会导致意外的行为——一个错误。

OOP 语言提供了避免这种情况的机制。首先在句法层面。在语义级别也是如此,这取决于编程语言:一部分语义可以使用断言编码在程序的文本中。但是确保子类是子类型取决于您。

还记得你是从什么时候开始学习 OOP 的吗? “如果关系是 IS-A,则使用继承”。反之亦然:如果您使用继承,请确保关系是 IS-A。

LSP 在比断言更高的级别上定义了什么是子类型。断言是确保 LSP 得到维护的宝贵工具。


T
Tom Hawtin - tackline

用一组 Board 来实现 ThreeDBoard 会有用吗?

也许您可能希望将各个平面上的 ThreeDBoard 切片视为 Board。在这种情况下,您可能希望为 Board 抽象出一个接口(或抽象类)以允许多个实现。

在外部接口方面,您可能需要为 TwoDBoard 和 ThreeDBoard 考虑一个 Board 接口(尽管上述方法都不适合)。


我认为这个例子只是为了证明在 ThreeDBoard 的上下文中从 board 继承是没有意义的,并且所有的方法签名对于 Z 轴都是没有意义的。
P
Prasa

到目前为止,我发现的 LSP 最清晰的解释是“Liskov 替换原则说,派生类的对象应该能够替换基类的对象,而不会在系统中带来任何错误或修改基类的行为"来自here。本文给出了违反 LSP 并修复它的代码示例。


请提供stackoverflow上的代码示例。
i
inf3rno

假设我们在代码中使用了一个矩形

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

在我们的几何课中,我们了解到正方形是一种特殊类型的矩形,因为它的宽度与高度相同。让我们也根据此信息创建一个 Square 类:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

如果我们在第一个代码中将 Rectangle 替换为 Square,那么它将中断:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

这是因为 Square 有一个我们在 Rectangle 类中没有的新前提条件:width == height。根据 LSP,Rectangle 实例应该可以替换为 Rectangle 子类实例。这是因为这些实例通过了 Rectangle 实例的类型检查,因此它们会导致代码中出现意外错误。

这是 wiki article“先决条件不能在子类型中加强” 部分的示例。所以总而言之,违反 LSP 可能会在某些时候导致代码中的错误。


Z
Zahra.HY

LSP 说“对象应该可以被它们的子类型替换”。另一方面,这一原则指向

子类不应该破坏父类的类型定义。

下面的例子有助于更好地理解 LSP。

没有 LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

通过 LSP 修复:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

R
Ryszard Dżegan

我鼓励您阅读这篇文章:Violating Liskov Substitution Principle (LSP)

您可以在其中找到什么是 Liskov 替换原则的解释、帮助您猜测是否已经违反它的一般线索以及帮助您使您的类层次结构更安全的方法示例。


R
Raghu Reddy Muttana

LISKOV SUBSTITUTION PRINCIPLE(来自 Mark Seemann 的书)指出,我们应该能够在不破坏客户端或实现的情况下将接口的一个实现替换为另一个实现。正是这一原则能够解决未来出现的需求,即使我们可以。今天无法预见它们。

如果我们把电脑从墙上拔下来(实现),墙上的插座(接口)和电脑(客户端)都不会坏(事实上,如果是笔记本电脑,它甚至可以用电池运行一段时间) .然而,对于软件,客户通常希望服务可用。如果服务被删除,我们会收到 NullReferenceException。为了处理这种情况,我们可以创建一个“什么都不做”的接口实现。这是一种称为 Null Object [4] 的设计模式,它大致对应于将计算机从墙上拔下。因为我们使用的是松散耦合,所以我们可以用什么都不做而不会造成麻烦的东西代替真正的实现。