协慌网

登录 贡献 社区

Liskov 替代原则的例子是什么?

我听说 Liskov 替换原则(LSP)是面向对象设计的基本原则。它是什么以及它的使用例子是什么?

答案

一个很好的例子说明了 LSP(我最近在一个播客中由 Bob 叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用。

在数学中, Square是一个Rectangle 。实际上它是一个矩形的专业化。 “是一个” 让你想要继承模型。但是,如果在代码中使SquareRectangle派生,则Square应该可以在任何您期望Rectangle位置使用。这会产生一些奇怪的行为。

想象一下你在Rectangle基类上有SetWidthSetHeight方法; 这似乎完全符合逻辑。但是,如果您的Rectangle引用指向Square ,则SetWidthSetHeight没有意义,因为设置一个将更改另一个以匹配它。在这种情况下, Square无法使用Rectangle进行 Liskov 替换测试,并且从Rectangle继承Square的抽象是不好的。

你们应该查看其他无价的SOLID 原理励志海报

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

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

从本质上讲,LSP 是关于接口和契约以及如何决定何时扩展一个类而不是使用另一个策略(如组合)来实现您的目标。

我所看到的最有效的方式就是Head First OOA&D 。他们提出了一个场景,您是一个项目开发人员,为战略游戏构建框架。

他们提出了一个代表董事会的类,如下所示:

类图

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

这本书继续改变要求,说游戏框架工作也必须支持 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。

LSP 涉及不变量。

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

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

class Square : Rectangle { }

现在我们遇到了一个问题,尽管界面匹配。原因是我们违反了由正方形和矩形的数学定义产生的不变量。 getter 和 setter 的工作方式, Rectangle应该满足以下不变量:

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

但是, 必须通过Square的正确实现来违反此不变量,因此它不是Rectangle的有效替代。