里氏替换原理的例子是什么?

发布于 2024-07-05 04:54:38 字数 51 浏览 9 评论 0 原文

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

I have heard that the Liskov Substitution Principle (LSP) is a fundamental principle of object oriented design. What is it and what are some examples of its use?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(30

反差帅 2024-07-12 04:54:38

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

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

This principle was introduced by Barbara Liskov in 1987 and extends the Open-Closed Principle by focusing on the behavior of a superclass and its subtypes.

Its importance becomes obvious when we consider the consequences of violating it. Consider an application that uses the following class.

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; 
    } 
  } 
}

Imagine that one day, the client demands the ability to manipulate squares in addition to rectangles. Since a square is a rectangle, the square class should be derived from the Rectangle class.

public class Square : Rectangle
{
} 

However, by doing that we will encounter two problems:

A square does not need both height and width variables inherited from the rectangle and this could create a significant waste in memory if we have to create hundreds of thousands of square objects.
The width and height setter properties inherited from the rectangle are inappropriate for a square since the width and height of a square are identical.
In order to set both height and width to the same value, we can create two new properties as follows:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Now, when someone will set the width of a square object, its height will change accordingly and vice-versa.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Let's move forward and consider this other function:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

If we pass a reference to a square object into this function, we would violate the LSP because the function does not work for derivatives of its arguments. The properties width and height aren't polymorphic because they aren't declared virtual in rectangle (the square object will be corrupted because the height won't be changed).

However, by declaring the setter properties to be virtual we will face another violation, the OCP. In fact, the creation of a derived class square is causing changes to the base class rectangle.

怎会甘心 2024-07-12 04:54:38

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

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

想象一下,您的 Rectangle 基类上有 SetWidthSetHeight 方法; 这看起来完全符合逻辑。 但是,如果您的 Rectangle 引用指向 Square,则 SetWidthSetHeight 没有意义,因为设置一个会改变另一个来匹配它。 在这种情况下,Square 未能通过 Rectangle 的里氏替换测试,并且从 Rectangle 继承 Square 的抽象是不好的。

输入图像描述这里

你们都应该看看其他无价的用励志海报解释坚实的原则

A great example illustrating LSP (given by Uncle Bob in a podcast I heard recently) was how sometimes something that sounds right in natural language doesn't quite work in code.

In mathematics, a Square is a Rectangle. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square derive from Rectangle, then a Square should be usable anywhere you expect a Rectangle. This makes for some strange behavior.

Imagine you had SetWidth and SetHeight methods on your Rectangle base class; this seems perfectly logical. However if your Rectangle reference pointed to a Square, then SetWidth and SetHeight doesn't make sense because setting one would change the other to match it. In this case Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.

enter image description here

Y'all should check out the other priceless SOLID Principles Explained With Motivational Posters.

夜血缘 2024-07-12 04:54:38

里氏替换原则(LSP,)是面向对象编程中的概念指出:

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

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

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

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

Class Diagram

所有方法都采用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。

The Liskov Substitution Principle (LSP, ) is a concept in Object Oriented Programming that states:

Functions that use pointers or
references to base classes must be
able to use objects of derived classes
without knowing it.

At its heart LSP is about interfaces and contracts as well as how to decide when to extend a class vs. use another strategy such as composition to achieve your goal.

The most effective way I have seen to illustrate this point was in Head First OOA&D. They present a scenario where you are a developer on a project to build a framework for strategy games.

They present a class that represents a board that looks like this:

Class Diagram

All of the methods take X and Y coordinates as parameters to locate the tile position in the two-dimensional array of Tiles. This will allow a game developer to manage units in the board during the course of the game.

The book goes on to change the requirements to say that the game frame work must also support 3D game boards to accommodate games that have flight. So a ThreeDBoard class is introduced that extends Board.

At first glance this seems like a good decision. Board provides both the Height and Width properties and ThreeDBoard provides the Z axis.

Where it breaks down is when you look at all the other members inherited from Board. The methods for AddUnit, GetTile, GetUnits and so on, all take both X and Y parameters in the Board class but the ThreeDBoard needs a Z parameter as well.

So you must implement those methods again with a Z parameter. The Z parameter has no context to the Board class and the inherited methods from the Board class lose their meaning. A unit of code attempting to use the ThreeDBoard class as its base class Board would be very out of luck.

Maybe we should find another approach. Instead of extending Board, ThreeDBoard should be composed of Board objects. One Board object per unit of the Z axis.

This allows us to use good object oriented principles like encapsulation and reuse and doesn’t violate LSP.

顾铮苏瑾 2024-07-12 04:54:38

Robert Martin 有一篇出色的 论文里氏替换原则。 它讨论了可能违反该原则的微妙和不那么微妙的方式。

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

违反 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 类的应用程序,如下所述
如下:

类矩形 
  { 
    民众: 
      void SetWidth(double w) {itsWidth=w;} 
      void SetHeight(double h) {itsHeight=w;} 
      double GetHeight() const {返回其高度;} 
      double GetWidth() const {返回其宽度;} 
    私人的: 
      双倍其宽度; 
      其高度加倍; 
  }; 
  

[...]想象一下,有一天用户需要能够操纵
除了长方形之外还有正方形。 [...]

显然,对于所有正常的意图和目的来说,正方形都是矩形。
由于 ISA 关系成立,因此对 Square 进行建模是合乎逻辑的
类派生自Rectangle。 [...]

Square 将继承 SetWidthSetHeight 函数。 这些
函数对于 Square 来说是完全不合适的,因为宽度和
正方形的高相同。 这应该是一个重要的线索
设计有问题。 不过,有一种方法可以
回避问题。 我们可以重写 SetWidthSetHeight [...]

但请考虑以下函数:

void f(矩形&r) 
  { 
    r.SetWidth(32);   // 调用 Rectangle::SetWidth 
  } 
  

如果我们将对 Square 对象的引用传递给此函数,则
Square 对象将被损坏,因为高度不会改变。
这明显违反了LSP。 该功能不适用于
其论点的衍生物。

[...]

Robert Martin has an excellent paper on the Liskov Substitution Principle. It discusses subtle and not-so-subtle ways in which the principle may be violated.

Some relevant parts of the paper (note that the second example is heavily condensed):

A Simple Example of a Violation of LSP

One of the most glaring violations of this principle is the use of C++
Run-Time Type Information (RTTI) to select a function based upon the
type of an object. i.e.:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Clearly the DrawShape function is badly formed. It must know about
every possible derivative of the Shape class, and it must be changed
whenever new derivatives of Shape are created. Indeed, many view the structure of this function as anathema to Object Oriented Design.

Square and Rectangle, a More Subtle Violation.

However, there are other, far more subtle, ways of violating the LSP.
Consider an application which uses the Rectangle class as described
below:

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;
    double itsHeight;
};

[...] Imagine that one day the users demand the ability to manipulate
squares in addition to rectangles. [...]

Clearly, a square is a rectangle for all normal intents and purposes.
Since the ISA relationship holds, it is logical to model the Square
class as being derived from Rectangle. [...]

Square will inherit the SetWidth and SetHeight functions. These
functions are utterly inappropriate for a Square, since the width and
height of a square are identical. This should be a significant clue
that there is a problem with the design. However, there is a way to
sidestep the problem. We could override SetWidth and SetHeight [...]

But consider the following function:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

If we pass a reference to a Square object into this function, the
Square object will be corrupted because the height won’t be changed.
This is a clear violation of LSP. The function does not work for
derivatives of its arguments.

[...]

静若繁花 2024-07-12 04:54:38

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)
}

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

LSP concerns invariants.

The classic example is given by the following pseudo-code declaration (implementations omitted):

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 { }

Now we have a problem although the interface matches. The reason is that we have violated invariants stemming from the mathematical definition of squares and rectangles. The way getters and setters work, a Rectangle should satisfy the following invariant:

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

However, this invariant (as well as the explicit postconditions) must be violated by a correct implementation of Square, therefore it is not a valid substitute of Rectangle.

一口甜 2024-07-12 04:54:38

可替换性是面向对象编程中的一个原则,指出在计算机程序中,如果 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 FlyingBird extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBird{}
public class Ostrich extends Bird{} 

Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S

Let's do a simple example in Java:

Bad example

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

The duck can fly because it is a bird, but what about this:

public class Ostrich extends Bird{}

Ostrich is a bird, but it can't fly, Ostrich class is a subtype of class Bird, but it shouldn't be able to use the fly method, that means we are breaking the LSP principle.

Good example

public class Bird{}
public class FlyingBird extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBird{}
public class Ostrich extends Bird{} 
送你一个梦 2024-07-12 04:54:38

我们用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() 方法。

这些都是违反里氏替换的问题
原则导致,并且它们通常可以被
方法什么都不做,甚至无法实现。

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

我们可以重构 TransportDevice 类,如下所示:

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() { ... }
}

并将 TransportDevice 扩展为机动设备。 这里添加Engine对象是比较合适的。

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

   void startEngine() { ... }
}

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

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

而我们的 Bicycle 类也符合里氏替换原则。

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

Let’s illustrate in 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() { ... }
}

There is no problem here, right? A car is definitely a transportation device, and here we can see that it overrides the startEngine() method of its superclass.

Let’s add another transportation device:

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

Everything isn’t going as planned now! Yes, a bicycle is a transportation device, however, it does not have an engine and hence, the method startEngine() cannot be implemented.

These are the kinds of problems that violation of Liskov Substitution
Principle leads to, and they can most usually be recognized by a
method that does nothing, or even can’t be implemented.

The solution to these problems is a correct inheritance hierarchy, and in our case we would solve the problem by differentiating classes of transportation devices with and without engines. Even though a bicycle is a transportation device, it doesn’t have an engine. In this example our definition of transportation device is wrong. It should not have an engine.

We can refactor our TransportationDevice class as follows:

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

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

Now we can extend TransportationDevice for non-motorized devices.

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

And extend TransportationDevice for motorized devices. Here is is more appropriate to add the Engine object.

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

   void startEngine() { ... }
}

Thus our Car class becomes more specialized, while adhering to the Liskov Substitution Principle.

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

And our Bicycle class is also in compliance with the Liskov Substitution Principle.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
花开雨落又逢春i 2024-07-12 04:54:38

有一个清单可以确定您是否违反了里氏规则。

  • 如果您违反以下一项-> 你违反了利斯科夫。
  • 如果您没有违反任何-> 无法得出任何结论。

检查列表:

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

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

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

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

  • 历史约束:重写方法时,不允许修改基类中不可修改的属性。 看一下这些代码,您可以看到 Name 被定义为不可修改(私有集),但 SubType 引入了允许修改它的新方法(通过反射):

     公共类 SuperType 
       { 
           公共字符串名称{获取;   私人套装;   } 
           公共 SuperType(字符串名称,整数年龄) 
           { 
               姓名=姓名; 
               年龄=年龄; 
           } 
       } 
       公共类子类型:超级类型 
       { 
           公共无效更改名称(字符串新名称) 
           { 
               var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName); 
           } 
       } 
      

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

There is a checklist to determine whether or not you are violating Liskov.

  • If you violate one of the following items -> you violate Liskov.
  • If you don't violate any -> can't conclude anything.

Check list:

  • No new exceptions should be thrown in derived class: If your base class threw ArgumentNullException then your sub classes were only allowed to throw exceptions of type ArgumentNullException or any exceptions derived from ArgumentNullException. Throwing IndexOutOfRangeException is a violation of Liskov.

  • Pre-conditions cannot be strengthened: Assume your base class works with a member int. Now your sub-type requires that int to be positive. This is strengthened pre-conditions, and now any code that worked perfectly fine before with negative ints is broken.

  • Post-conditions cannot be weakened: Assume your base class required all connections to the database should be closed before the method returned. In your sub-class you overrode that method and left the connection open for further reuse. You have weakened the post-conditions of that method.

  • Invariants must be preserved: The most difficult and painful constraint to fulfill. Invariants are sometimes hidden in the base class and the only way to reveal them is to read the code of the base class. Basically you have to be sure when you override a method anything unchangeable must remain unchanged after your overridden method is executed. The best thing I can think of is to enforce these invariant constraints in the base class but that would not be easy.

  • History Constraint: When overriding a method you are not allowed to modify an unmodifiable property in the base class. Take a look at these code and you can see Name is defined to be unmodifiable (private set) but SubType introduces new method that allows modifying it (through reflection):

     public class SuperType
     {
         public string Name { get; private set; }
         public SuperType(string name, int age)
         {
             Name = name;
             Age = age;
         }
     }
     public class SubType : SuperType
     {
         public void ChangeName(string newName)
         {
             var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
         }
     }
    

There are 2 others items: Contravariance of method arguments and Covariance of return types. But it is not possible in C# (I'm a C# developer) so I don't care about them.

梦里梦着梦中梦 2024-07-12 04:54:38

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

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

I see rectangles and squares in every answer, and how to violate the LSP.

I'd like to show how the LSP can be conformed to with a real-world example :

<?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; 
    }
}

This design conforms to the LSP because the behaviour remains unchanged regardless of the implementation we choose to use.

And yes, you can violate LSP in this configuration doing one simple change like so :

<?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 !
    }
}

Now the subtypes cannot be used the same way since they don't produce the same result anymore.

素手挽清风 2024-07-12 04:54:38

当某些代码认为它正在调用 T 类型的方法,并且可能不知不觉地调用 S 类型的方法时,LSP 是必要的,其中 S 扩展了 T< /code> (即 S 继承、派生自超类型 T,或者是超类型 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

即使具有不可变字段的类型仍然具有不变量,例如不可变矩形设置器期望独立修改维度,但是不可变方形设置器违反了这一点期待。

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 的每个方法必须具有逆变输入参数和协变输出。

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

协方差表示方差与继承方向相同,即子类型S的每个方法的输出的类型So必须相同或者< em>subtype 类型 To 是超类型 T 相应方法的相应输出。

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

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

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


子类型适用于可以枚举不变量的情况

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

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

由于需要语义抽象优于扩展,我希望使用类型来建模不变量,即统一的高阶指称语义,优于类型状态。 “扩展”意味着不协调、模块化开发的无限制、排列组合。 因为在我看来,拥有两个相互依赖的模型(例如类型和类型状态)来表达共享语义是统一和自由度的对立面,而这两个模型不能相互统一以实现可扩展的组合。 例如,Expression Problem之类的扩展在子类型、函数重载和参数类型方面得到了统一域。

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

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

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

  • 哥德尔不完备性定理:任何可以证明所有算术真理的形式理论都是不一致的。
  • 罗素悖论:每个成员资格规则set 可以包含一个集合,要么枚举每个成员的特定类型,要么包含其自身。 因此,集合要么不能扩展,要么是无界递归。 例如,不是茶壶的所有东西的集合,包括它自己,它包括它自己,它包括它自己,等等……。 因此,如果规则(可能包含集合并且)不枚举特定类型(即允许所有未指定类型)并且不允许无限制扩展,则该规则是不一致的。 这是一组不属于其自身的集合。 这种无法在所有可能的扩展上保持一致和完全枚举的情况,就是哥德尔的不完备性定理。
  • 里氏替换原理:任何集合是否是另一个集合的子集一般都是不可判定问题,即继承一般是不可判定的。
  • 林斯基参考:当某个事物被描述或感知时,它的计算是什么是不可判定的,即感知(现实)没有绝对的参考点。
  • 科斯定理:不存在外部参考点,因此任何对无限外部可能性的障碍都将失败。
  • 热力学第二定律:整个宇宙(一个封闭的宇宙)系统(即一切)趋于最大无序,即最大独立可能性。

LSP is necessary where some code thinks it is calling the methods of a type T, and may unknowingly call the methods of a type S, where S extends T (i.e. S inherits, derives from, or is a subtype of, the supertype T).

For example, this occurs where a function with an input parameter of type T, is called (i.e. invoked) with an argument value of type S. Or, where an identifier of type T, is assigned a value of type S.

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

LSP requires the expectations (i.e. invariants) for methods of type T (e.g. Rectangle), not be violated when the methods of type S (e.g. Square) are called instead.

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

Even a type with immutable fields still has invariants, e.g. the immutable Rectangle setters expect dimensions to be independently modified, but the immutable Square setters violate this expectation.

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 requires that each method of the subtype S must have contravariant input parameter(s) and a covariant output.

Contravariant means the variance is contrary to the direction of the inheritance, i.e. the type Si, of each input parameter of each method of the subtype S, must be the same or a supertype of the type Ti of the corresponding input parameter of the corresponding method of the supertype T.

Covariance means the variance is in the same direction of the inheritance, i.e. the type So, of the output of each method of the subtype S, must be the same or a subtype of the type To of the corresponding output of the corresponding method of the supertype T.

This is because if the caller thinks it has a type T, thinks it is calling a method of T, then it supplies argument(s) of type Ti and assigns the output to the type To. When it is actually calling the corresponding method of S, then each Ti input argument is assigned to a Si input parameter, and the So output is assigned to the type To. Thus if Si were not contravariant w.r.t. to Ti, then a subtype Xi—which would not be a subtype of Si—could be assigned to Ti.

Additionally, for languages (e.g. Scala or Ceylon) which have definition-site variance annotations on type polymorphism parameters (i.e. generics), the co- or contra- direction of the variance annotation for each type parameter of the type T must be opposite or same direction respectively to every input parameter or output (of every method of T) that has the type of the type parameter.

Additionally, for each input parameter or output that has a function type, the variance direction required is reversed. This rule is applied recursively.


Subtyping is appropriate where the invariants can be enumerated.

There is much ongoing research on how to model invariants, so that they are enforced by the compiler.

Typestate (see page 3) declares and enforces state invariants orthogonal to type. Alternatively, invariants can be enforced by converting assertions to types. For example, to assert that a file is open before closing it, then File.open() could return an OpenFile type, which contains a close() method that is not available in File. A tic-tac-toe API can be another example of employing typing to enforce invariants at compile-time. The type system may even be Turing-complete, e.g. Scala. Dependently-typed languages and theorem provers formalize the models of higher-order typing.

Because of the need for semantics to abstract over extension, I expect that employing typing to model invariants, i.e. unified higher-order denotational semantics, is superior to the Typestate. ‘Extension’ means the unbounded, permuted composition of uncoordinated, modular development. Because it seems to me to be the antithesis of unification and thus degrees-of-freedom, to have two mutually-dependent models (e.g. types and Typestate) for expressing the shared semantics, which can't be unified with each other for extensible composition. For example, Expression Problem-like extension was unified in the subtyping, function overloading, and parametric typing domains.

My theoretical position is that for knowledge to exist (see section “Centralization is blind and unfit”), there will never be a general model that can enforce 100% coverage of all possible invariants in a Turing-complete computer language. For knowledge to exist, unexpected possibilities much exist, i.e. disorder and entropy must always be increasing. This is the entropic force. To prove all possible computations of a potential extension, is to compute a priori all possible extension.

This is why the Halting Theorem exists, i.e. it is undecidable whether every possible program in a Turing-complete programming language terminates. It can be proven that some specific program terminates (one which all possibilities have been defined and computed). But it is impossible to prove that all possible extension of that program terminates, unless the possibilities for extension of that program is not Turing complete (e.g. via dependent-typing). Since the fundamental requirement for Turing-completeness is unbounded recursion, it is intuitive to understand how Gödel's incompleteness theorems and Russell's paradox apply to extension.

An interpretation of these theorems incorporates them in a generalized conceptual understanding of the entropic force:

  • Gödel's incompleteness theorems: any formal theory, in which all arithmetic truths can be proved, is inconsistent.
  • Russell's paradox: every membership rule for a set that can contain a set, either enumerates the specific type of each member or contains itself. Thus sets either cannot be extended or they are unbounded recursion. For example, the set of everything that is not a teapot, includes itself, which includes itself, which includes itself, etc…. Thus a rule is inconsistent if it (may contain a set and) does not enumerate the specific types (i.e. allows all unspecified types) and does not allow unbounded extension. This is the set of sets that are not members of themselves. This inability to be both consistent and completely enumerated over all possible extension, is Gödel's incompleteness theorems.
  • Liskov Substition Principle: generally it is an undecidable problem whether any set is the subset of another, i.e. inheritance is generally undecidable.
  • Linsky Referencing: it is undecidable what the computation of something is, when it is described or perceived, i.e. perception (reality) has no absolute point of reference.
  • Coase's theorem: there is no external reference point, thus any barrier to unbounded external possibilities will fail.
  • Second law of thermodynamics: the entire universe (a closed system, i.e. everything) trends to maximum disorder, i.e. maximum independent possibilities.
爱人如己 2024-07-12 04:54:38

故事简短,让我们留下矩形矩形和正方形正方形,扩展父类时的实际示例,您必须保留确切的父 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 并依赖它的 Client

/**
 * 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();
    }
} 

子类替换类破坏了API的契约时,LSP就会被破坏。

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/

Long story short, let's leave rectangles rectangles and squares squares, practical example when extending a parent class, you have to either PRESERVE the exact parent API or to EXTEND IT.

Let's say you have a base ItemsRepository.

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

        return $numberOfDeletedRows;
    }
}

And a sub class extending it:

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;
    }
}

Then you could have a Client working with the Base ItemsRepository API and relying on it.

/**
 * 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();
    }
} 

The LSP is broken when substituting parent class with a sub class breaks the API's contract.

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 :(
    }
}

You can learn more about writing maintainable software in my course: https://www.udemy.com/enterprise-php/

我一向站在原地 2024-07-12 04:54:38

我想每个人都已经了解了 LSP 在技术上的含义:您基本上希望能够从子类型细节中抽象出来并安全地使用超类型。

所以里氏有 3 个基本规则:

  1. 签名规则:在语法上,子类型中的超类型的每个操作都应该有一个有效的实现。 编译器可以为您检查一些内容。 有一条关于抛出更少异常并且至少与超类型方法一样可访问的小规则。

  2. 方法规则:这些操作的实现在语义上是合理的。

    • 较弱的先决条件:子类型函数应该至少采用超类型作为输入的内容(如果不是更多的话)。
    • 更强的后置条件:它们应该产生超类型方法产生的输出的子集。
  3. 属性规则:这超出了单个函数调用的范围。

    • 不变量:始终正确的事情必须保持正确。 例如。 Set 的大小永远不会是负数。
    • 进化属性:通常与不变性或对象可以处于的状态类型有关。或者对象可能只增长而从不收缩,因此子类型方法不应该实现它。

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

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

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

I guess everyone kind of covered what LSP is technically: You basically want to be able to abstract away from subtype details and use supertypes safely.

So Liskov has 3 underlying rules:

  1. Signature Rule : There should be a valid implementation of every operation of the supertype in the subtype syntactically. Something a compiler will be able to check for you. There is a little rule about throwing fewer exceptions and being at least as accessible as the supertype methods.

  2. Methods Rule: The implementation of those operations is semantically sound.

    • Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more.
    • Stronger Postconditions: They should produce a subset of the output the supertype methods produced.
  3. Properties Rule : This goes beyond individual function calls.

    • Invariants : Things that are always true must remain true. Eg. a Set's size is never negative.
    • Evolutionary Properties : Usually something to do with immutability or the kind of states the object can be in. Or maybe the object only grows and never shrinks so the subtype methods shouldn't make it.

All these properties need to be preserved and the extra subtype functionality shouldn't violate supertype properties.

If these three things are taken care of , you have abstracted away from the underlying stuff and you are writing loosely coupled code.

Source: Program Development in Java - Barbara Liskov

零崎曲识 2024-07-12 04:54:38

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

在 Pseudo-python 中,

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

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

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

The LSP is a rule about the contract of the clases: if a base class satisfies a contract, then by the LSP derived classes must also satisfy that contract.

In Pseudo-python

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

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

satisfies LSP if every time you call Foo on a Derived object, it gives exactly the same results as calling Foo on a Base object, as long as arg is the same.

往日情怀 2024-07-12 04:54:38

里氏替换原则

  • 重写的方法不应保留为空
  • 重写的方法不应引发错误
  • 基类或接口行为不应因为派生类行为而进行修改(返工)。

The Liskov Substitution Principle

  • The overridden method shouldn’t remain empty
  • The overridden method shouldn’t throw an error
  • Base class or interface behavior should not go for modification (rework) as because of derived class behaviors.
木落 2024-07-12 04:54:38

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

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

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

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

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

有了这些知识,评估 LSP 遵守情况可以成为一个很好的工具,可以确定组合何时是扩展现有功能(而不是继承)更合适的机制。

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

When I first read about LSP, I assumed that this was meant in a very strict sense, essentially equating it to interface implementation and type-safe casting. Which would mean that LSP is either ensured or not by the language itself. For example, in this strict sense, ThreeDBoard is certainly substitutable for Board, as far as the compiler is concerned.

After reading up more on the concept though I found that LSP is generally interpreted more broadly than that.

In short, what it means for client code to "know" that the object behind the pointer is of a derived type rather than the pointer type is not restricted to type-safety. Adherence to LSP is also testable through probing the objects actual behavior. That is, examining the impact of an object's state and method arguments on the results of the method calls, or the types of exceptions thrown from the object.

Going back to the example again, in theory the Board methods can be made to work just fine on ThreeDBoard. In practice however, it will be very difficult to prevent differences in behavior that client may not handle properly, without hobbling the functionality that ThreeDBoard is intended to add.

With this knowledge in hand, evaluating LSP adherence can be a great tool in determining when composition is the more appropriate mechanism for extending existing functionality, rather than inheritance.

甜尕妞 2024-07-12 04:54:38

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

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

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

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

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

另请参阅 Stackoverflow 问题“我可以实现一系列可重用测试来测试接口的实现吗?”的答案

An important example of the use of LSP is in software testing.

If I have a class A that is an LSP-compliant subclass of B, then I can reuse the test suite of B to test A.

To fully test subclass A, I probably need to add a few more test cases, but at the minimum I can reuse all of superclass B's test cases.

A way to realize is this by building what McGregor calls a "Parallel hierarchy for testing": My ATest class will inherit from BTest. Some form of injection is then needed to ensure the test case works with objects of type A rather than of type B (a simple template method pattern will do).

Note that reusing the super-test suite for all subclass implementations is in fact a way to test that these subclass implementations are LSP-compliant. Thus, one can also argue that one should run the superclass test suite in the context of any subclass.

See also the answer to the Stackoverflow question "Can I implement a series of reusable tests to test an interface's implementation?"

葬シ愛 2024-07-12 04:54:38

简单来说,LSP 指出相同的对象超类应该能够在不破坏任何内容的情况下相互交换

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

The LSP in simple terms states that objects of the same superclass should be able to be swapped with each other without breaking anything.

For example, if we have a Cat and a Dog class derived from an Animal class, any functions using the Animal class should be able to use Cat or Dog and behave normally.

離殇 2024-07-12 04:54:38

用一句很简单的话来说,我们可以说:

子类不能违背它的基类特性。 它必须有能力做到这一点。 我们可以说它与子类型相同。

In a very simple sentence, we can say:

The child class must not violate its base class characteristics. It must be capable with it. We can say it's same as subtyping.

动次打次papapa 2024-07-12 04:54:38

里氏替换原理(LSP)

我们一直在设计一个程序模块并创建一些类
层次结构。 然后我们扩展一些类,创建一些派生类
类。

我们必须确保新的派生类只是扩展而无需
替换旧类的功能。 否则,新班级
在现有程序中使用时可能会产生不良效果
模块。

里氏替换原则指出,如果一个程序模块是
使用基类,那么对基类的引用可以是
替换为派生类而不影响其功能
程序模块。

示例:

下面是违反里氏替换原理的经典示例。 在示例中,使用了 2 个类:矩形和正方形。 假设在应用程序中的某个地方使用了 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.
    }
}

结论:

这个原则只是开闭原则的延伸,它
意味着我们必须确保新的派生类正在扩展
基类而不改变它们的行为。

另请参阅:开闭原则

一些类似的概念,以获得更好的结构: 约定优于配置

Liskov's Substitution Principle(LSP)

All the time we design a program module and we create some class
hierarchies. Then we extend some classes creating some derived
classes.

We must make sure that the new derived classes just extend without
replacing the functionality of old classes. Otherwise, the new classes
can produce undesired effects when they are used in existing program
modules.

Liskov's Substitution Principle states that if a program module is
using a Base class, then the reference to the Base class can be
replaced with a Derived class without affecting the functionality of
the program module.

Example:

Below is the classic example for which the Liskov's Substitution Principle is violated. In the example, 2 classes are used: Rectangle and Square. Let's assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don't know the exact what type of object will be returned. But we know it's a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10, the area should be 50. Instead, the result will be 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.
    }
}

Conclusion:

This principle is just an extension of the Open Close Principle and it
means that we must make sure that new derived classes are extending
the base classes without changing their behavior.

See also: Open Close Principle

Some similar concepts for better structure: Convention over configuration

十年不长 2024-07-12 04:54:38

LSP 的这个表述太强了:

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

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

所以,基本上,任何后期绑定的使用都会违反 LSP 。 OO 的全部意义在于,当我们用一种对象替换另一种对象时,会获得不同的行为!

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

This formulation of the LSP is way too strong:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.

Which basically means that S is another, completely encapsulated implementation of the exact same thing as T. And I could be bold and decide that performance is part of the behavior of P...

So, basically, any use of late-binding violates the LSP. It's the whole point of OO to to obtain a different behavior when we substitute an object of one kind for one of another kind!

The formulation cited by wikipedia is better since the property depends on the context and does not necessarily include the whole behavior of the program.

唯憾梦倾城 2024-07-12 04:54:38

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

  • 派生类必须保留基类的不变体
  • 派生类不得增强基类的先决条件
  • 基类的后置条件不得被派生类削弱。

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

有关此问题的进一步讨论,请参见我的博客:里氏替换原理< /a>

Some addendum:
I wonder why didn't anybody write about the Invariant , preconditions and post conditions of the base class that must be obeyed by the derived classes.
For a derived class D to be completely sustitutable by the Base class B, class D must obey certain conditions:

  • In-variants of base class must be preserved by the derived class
  • Pre-conditions of the base class must not be strengthened by the derived class
  • Post-conditions of the base class must not be weakened by the derived class.

So the derived must be aware of the above three conditions imposed by the base class. Hence, the rules of subtyping are pre-decided. Which means, 'IS A' relationship shall be obeyed only when certain rules are obeyed by the subtype. These rules, in the form of invariants, precoditions and postcondition, should be decided by a formal 'design contract'.

Further discussions on this available at my blog: Liskov Substitution principle

花桑 2024-07-12 04:54:38

它指出,如果 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{}

It states that if C is a subtype of E then E can be replaced with objects of type C without changing or breaking the behavior of the program. In simple words, derived classes should be substitutable for their parent classes. For example, if a Farmer’s son is Farmer then he can work in place of his father but if a Farmer’s son is a cricketer then he can’t work in place of his father.

Violation Example:

public class Plane{

  public void startEngine(){}      

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

In the given example FighterPlane and PaperPlane classes both extending the Plane class which contain startEngine() method. So it's clear that FighterPlane can start engine but PaperPlane can’t so it’s breaking LSP.

PaperPlane class although extending Plane class and should be substitutable in place of it but is not an eligible entity that Plane’s instance could be replaced by, because a paper plane can’t start the engine as it doesn’t have one. So the good example would be,

Respected Example:

public class Plane{ 
} 
public class RealPlane{

  public void startEngine(){} 

}
public class FighterJet extends RealPlane{} 
public class PaperPlane extends Plane{}
浅忆流年 2024-07-12 04:54:38

大局观:

  • 里氏替换原理是什么? 它是关于什么是(什么不是)给定类型的子类型
  • 为什么它如此重要? 因为子类型子类之间是有区别的。

示例

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

CircleColoredCircle

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

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 公告,23,5(1988 年 5 月))

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

我们如何找到 o2 取决于 o1 ? 我们只删除 color 属性并保留 radius 属性。 我将这个转换称为o1 -> o2CircleColor 空间到 Circle 空间的投影

反例

让我们再创建一个例子来说明 LSP 的违规情况。

CircleSquare

想象一下前面的 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 的子类型code> 因为常规 Circle 实例的半径不会为 -1。

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

我们有一个根据 Circle 定义的程序,并且没有 Circle 对象可以替换 new Square(2) (或任何 顺便说一下,此程序中的 Square 实例)并保持行为不变:记住任何Circle 的半径始终为正。

子类和子类型

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

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

静态类型

如果有一些静态类型,您将在编译时受到超类签名的约束。 Square.getRadius() 无法返回 StringList

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

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

按契约设计

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

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

  • if 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)。
  • 后置条件定义什么是有效结果。 当子类化一个类时,后置条件只能得到加强(SfTf 提供更多)(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 得到支持的宝贵工具。

The big picture :

  • What is Liskov Substitution Principle about ? It's about what is (and what is not) a subtype of a given type.
  • Why is it so important ? Because there is a difference between a subtype and a subclass.

Example

Unlike the other answers, I won't start with a Liskov Substitution Principle (LSP) violation, but with a LSP compliance. I use Java but it would be almost the same in every OOP language.

Circle and ColoredCircle

Geometrical examples seem pretty popular here.

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;
    }
}

The radius is not allowed to be negative. Here's a suclass:

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;
    }
}

This subclass is a subtype of Circle, according to the LSP.

The LSP states that:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. (Barbara Liskov, "Data Abstraction and Hierarchy", SIGPLAN Notices, 23,5 (May, 1988))

Here, for each ColoredCircle instance o1, consider the Circle instance having the same radius o2. For every program using Circle objects, if you replace o2 by o1, the behavior of any program using Circle will remain the same after the substitution. (Note that this is theoretical : you will exhaust the memory faster using ColoredCircle instances than using Circle instances, but that's not relevant here.)

How do we find the o2 depending on o1 ? We just strip the color attribute and keep the radius attribute. I call the transformation o1 -> o2 a projection from the CircleColor space on the Circle space.

Counter Example

Let's create another example to illustrate the violation of the LSP.

Circle and Square

Imagine this subclass of the previous Circle class:

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;
    }
}

The violation of the LSP

Now, look at this program :

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

We test the program with a Circle object and with a Square object.

    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"
    }
}

What happened ? Intuitively, although Square is a subclass of Circle, Square is not a subtype of Circle because no regular Circle instance would ever have a radius of -1.

Formally, this is a violation of Liskov Substitution Principle.

We have a program defined in terms of Circle and there is no Circle object that can replace new Square(2) (or any Square instance by the way) in this program and leave the behavior unchanged: remember that radius of any Circle is always positive.

Subclass and subtype

Now we know why a subclass is not always subtype. When a subclass is not a subtype, i.e. when there is a LSP violation, the behavior of some programs (at least one) won't always be the expected behavior. This is very frustrating and is usually interpreted as a bug.

In an ideal world, the compiler or interpreter would be able to check is a given subclass is a real subtype, but we are not in an ideal world.

Static typing

If there is some static typing, you are bound by the superclass signature at compile time. Square.getRadius() can't return a String or a List.

If there is no static typing, you'll get an error at runtime if the type of one argument is wrong (unless the typing is weak) or the number of arguments is inconsistent (unless the language is very permissive).

Note about the static typing: there is a mechanism of covariance of the return type (a method of S can return a subclass of the return type of the same method of T) and contravariance of the parameters types (a method of S can accept a superclass of a parameter of the same parameter of the same method of T). That is a specific case of precondition and postcondition explained below.

Design by contract

There's more. Some languages (I think of Eiffel) provide a mechanism to enforce the compliance with the LSP.

Let alone the determination the projection o2 of the initial object o1, we can expect the same behavior of any program if o1 is substituted for o2 if, for any argument x and any method f:

  • if o2.f(x) is a valid call, then o1.f(x) should also be a valid call (1).
  • the result (return value, display on console, etc.) of o1.f(x) should be equal to the result of o2.f(x), or at least equally valid (2).
  • o1.f(x) should let o1 in an internal state and o2.f(x) should let o2 in an internal state so that next function calls will ensure that (1), (2) and (3) will still be valid (3).

(Note that (3) is given for free if the function f is pure. That's why we like to have immutable objects.)

These conditions are about the semantics (what to expect) of the class, not only the syntax of the class. Also, these conditions are very strong. But they can be approximated by assertions in design by contract programming. These assertions are a way to ensure that the semantic of the type is upheld. Breaking the contract leads to runtime errors.

  • The precondition defines what is a valid call. When subclassing a class, the precondition may only be weakened (S.f accepts more than T.f) (a).
  • The postcondition defines what is a valid result. When subclassing a class, the postcondition may only be strengthened (S.f provides more than T.f) (b).
  • The invariant defines what is a valid internal state. When subclassing a class, the invariant must remain the same (c).

We see that, roughly, (a) ensures (1) and (b) ensures (2), but (c) is weaker than (3). Moreover, assertions are sometimes difficult to express.

Think of a class Counter having a unique method Counter.counter() that returns the next integer. How do you write a postcondition for that ? Think of a class Random having a method Random.gaussian() that returns a float between 0.0 and 1.0 . How do you write a postcondition to check that the distribution is gaussian ? It may be possible, but the cost would be so high that we would rely on test rather than on postconditions.

Conclusion

Unfortunately, a subclass is not always a subtype. This can lead to an unexpected behavior -- a bug.

OOP languages provide mechanism to avoid this situation. At syntactic level first. At semantical level too, depending on the programming language: a part of the semantics can be encoded in the text of the program using assertions. But it's up to you to ensure that a subclass is a subtype.

Remember when you began to learn OOP ? "If the relation is IS-A, then use inheritance". That's true the other way: if you use inheritance, be sure that the relation is IS-A.

The LSP defines, at a higher level than assertions, what is a subtype. Assertions are a valuable tool to ensure that the LSP is upheld.

蒲公英的约定 2024-07-12 04:54:38

假设我们在代码中使用矩形。

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 实例的类型检查,因此它们将在您的代码中导致意外错误。

这是 维基文章。 总而言之,违反 LSP 可能会在某些时候导致代码错误。

Let's say we use a rectangle in our code

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

In our geometry class we learned that a square is a special type of rectangle because its width is the same length as its height. Let's make a Square class as well based on this info:

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

If we replace the Rectangle with Square in our first code, then it will break:

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

This is because the Square has a new precondition we did not have in the Rectangle class: width == height. According to LSP the Rectangle instances should be substitutable with Rectangle subclass instances. This is because these instances pass the type check for Rectangle instances and so they will cause unexpected errors in your code.

This was an example for the "preconditions cannot be strengthened in a subtype" part in the wiki article. So to sum up, violating LSP will probably cause errors in your code at some point.

醉生梦死 2024-07-12 04:54:38

用 Board 数组来实现 ThreeDBoard 有用吗?

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

在外部接口方面,您可能希望为 TwoDBoard 和 ThreeDBoard 提取一个 Board 接口(尽管上述方法都不适合)。

Would implementing ThreeDBoard in terms of an array of Board be that useful?

Perhaps you may want to treat slices of ThreeDBoard in various planes as a Board. In that case you may want to abstract out an interface (or abstract class) for Board to allow for multiple implementations.

In terms of external interface, you might want to factor out a Board interface for both TwoDBoard and ThreeDBoard (although none of the above methods fit).

难理解 2024-07-12 04:54:38

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

The clearest explanation for LSP I found so far has been "The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class" from here. The article gives code example for violating LSP and fixing it.

冰葑 2024-07-12 04:54:38

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

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

A square is a rectangle where the width equals the height. If the square sets two different sizes for the width and height it violates the square invariant. This is worked around by introducing side effects. But if the rectangle had a setSize(height, width) with precondition 0 < height and 0 < width. The derived subtype method requires height == width; a stronger precondition (and that violates lsp). This shows that though square is a rectangle it is not a valid subtype because the precondition is strengthened. The work around (in general a bad thing) cause a side effect and this weakens the post condition (which violates lsp). setWidth on the base has post condition 0 < width. The derived weakens it with height == width.

Therefore a resizable square is not a resizable rectangle.

如果没有你 2024-07-12 04:54:38

里氏替换原则

[SOLID]

继承子类型化

Wiki Liskov 替换原则 (LSP)

无法在子类型中强化先决条件。
子类型中的后置条件不能被削弱。
超类型的不变量必须保留在子类型中。

  • 子类型不应要求调用者的(前置条件)大于超类型
  • 子类型不应为调用者暴露(后置条件)小于超类型

*Precondition + Postcondition = 函数(方法)类型[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)


  • 不变变量超级类型的[关于]应保持不变

Swift

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();
    }
}

保持不变行为子类型< /strong>

Wiki Liskov 替换原则 (LSP)

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

[方差、协方差、逆变、不变]

Liskov Substitution Principle

[SOLID]

Inheritance Subtyping

Wiki Liskov substitution principle (LSP)

Preconditions cannot be strengthened in a subtype.
Postconditions cannot be weakened in a subtype.
Invariants of the supertype must be preserved in a subtype.

  • Subtype should not require(Preconditions) from caller more than supertype
  • Subtype should not expose(Postconditions) for caller less than supertype

*Precondition + Postcondition = function (method) types[Swift Function type. Swift function vs method]

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

//function type
(Class1) -> Class2

//Precondition
Class1

//Postcondition
Class2

Example

//C3 -> C2 -> C1

class C1 {}
class C2: C1 {}
class C3: C2 {}
  • Preconditions(e.g. function parameter type) can be the same or weaker(strives for -> C1)

  • Postconditions(e.g. function returned type) can be the same or stronger(strives for -> C3)

  • Invariant variable[About] of super type should stay invariant

Swift

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();
    }
}

Behavioral subtyping

Wiki Liskov substitution principle (LSP)

Contravariance of method parameter types in the subtype.
Covariance of method return types in the subtype.
New exceptions cannot be thrown by the methods in the subtype, except if they are subtypes of exceptions thrown by the methods of the supertype.

[Variance, Covariance, Contravariance, Invariance]

情绪失控 2024-07-12 04:54:38

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();
}

LSP says that ''Objects should be replaceable by their subtypes''.
On the other hand, this principle points to

Child classes should never break the parent class`s type definitions.

and the following example helps to have a better understanding of LSP.

Without 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();
}

Fixing by 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();
}
本宫微胖 2024-07-12 04:54:38

我鼓励您阅读这篇文章:违反里氏替换原则 (LSP)

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

I encourage you to read the article: Violating Liskov Substitution Principle (LSP).

You can find there an explanation what is the Liskov Substitution Principle, general clues helping you to guess if you have already violated it and an example of approach that will help you to make your class hierarchy be more safe.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文