里斯科夫替代原则的一个例子是什么?

时间:2020-03-05 18:51:34  来源:igfitidea点击:

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

解决方案

回答

以董事会的形式实施ThreeDBoard会有用吗?

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

在外部接口方面,我们可能希望为TwoDBoard和ThreeDBoard都考虑一个Board接口(尽管以上方法都不适合)。

回答

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

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

LSP的核心是关于接口和协定,以及如何决定何时扩展类与使用其他策略(例如合成)来实现目标。

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

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

所有这些方法都以X和Y坐标为参数,以在" Tiles"的二维数组中定位图块的位置。这将允许游戏开发人员在游戏过程中管理棋盘中的单元。

本书继续更改要求,说游戏框架还必须支持3D游戏板以容纳具有飞行功能的游戏。因此引入了" ThreeDBoard"类,该类扩展了" Board"。

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

当我们查看所有从Board继承的其他成员时,它就会崩溃。 AddUnit,GetTile,GetUnits等方法在Board类中都使用X和Y参数,而ThreeDBoard也需要Z参数。

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

也许我们应该找到另一种方法。除了扩展Board之外,ThreeDBoard应该由Board对象组成。 Z轴的每单位一个"木板"对象。

这使我们能够使用良好的面向对象原理,例如封装和重用,并且不会违反LSP。

回答

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

当我第一次阅读有关LSP的文章时,我认为这是非常严格的意思,实质上等同于接口实现和类型安全的转换。这将意味着语言本身无法保证LSP。例如,从严格意义上讲,就编译器而言,ThreeDBoard当然可以替代Board。

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

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

再次回到该示例,理论上可以使Board方法在ThreeDBoard上正常工作。但是,实际上,如果不妨碍ThreeDBoard要添加的功能,很难防止客户端可能无法正确处理的行为差异。

掌握了这些知识之后,评估LSP的遵从性可能是确定组合何时是扩展现有功能(而不是继承)的更合适机制的好工具。

回答

罗伯特·马丁(Robert Martin)在《里斯科夫换人原则》上有出色的论文。它讨论了可能违反该原则的微妙和不太微妙的方式。

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

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. 
  
  [...]

回答

LSP关注不变性。

下面的伪代码声明(省略了实现)给出了经典示例:

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

现在,尽管接口匹配,我们还是有问题。原因是我们违反了基于正方形和矩形的数学定义的不变量。获取器和设置器的工作方式,"矩形"应满足以下不变式:

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

但是,必须通过正确的"平方"实现来违反此不变式,因此它不是"矩形"的有效替代品。

回答

LSP是有关租赁合同的规则:如果基类满足合同,则LSP派生的类也必须满足该合同。

在伪python中

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

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

如果每次我们在派生对象上调用Foo时,只要arg相同,它就会得到与在基础对象上调用Foo完全相同的结果。

回答

一个很好的例子说明了LSP(我最近听到的一个播客中的Bob叔叔给了LSP),就是有时候自然语言听起来有些正确,但在代码中却无法正常工作。

在数学中,"平方"是"矩形"。实际上,它是矩形的一种特殊形式。 "是"使我们想使用继承对其进行建模。但是,如果在代码中我们使"平方"源自"矩形",那么在希望"矩形"的任何地方都可以使用"平方"。这导致一些奇怪的行为。

假设我们在Rectangle基类上有SetWidthSetHeight方法;这似乎完全合乎逻辑。但是,如果"矩形"引用指向"平方",则" SetWidth"和" SetHeight"没有意义,因为设置一个会更改另一个以匹配它。在这种情况下,"正方形"未通过"矩形"通过Liskov替代测试,而使"正方形"继承自"矩形"的抽象是一个不好的选择。

大家都应该查看其他无价的SOLID Principles励志海报。

回答

奇怪的是,没有人发表过描述lsp的原始论文。这不是罗伯特·马丁(Robert Martin)的一本好书,但值得。

回答

LSP的这种公式过于强大:

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

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

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

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