我听说 Liskov 替换原则(LSP)是面向对象设计的基本原则。它是什么以及它的使用例子是什么?
一个很好的例子说明了 LSP(我最近在一个播客中由 Bob 叔叔给出的)是有时在自然语言中听起来不对的东西在代码中不起作用。
在数学中, Square
是一个Rectangle
。实际上它是一个矩形的专业化。 “是一个” 让你想要继承模型。但是,如果在代码中使Square
从Rectangle
派生,则Square
应该可以在任何您期望Rectangle
位置使用。这会产生一些奇怪的行为。
想象一下你在Rectangle
基类上有SetWidth
和SetHeight
方法; 这似乎完全符合逻辑。但是,如果您的Rectangle
引用指向Square
,则SetWidth
和SetHeight
没有意义,因为设置一个将更改另一个以匹配它。在这种情况下, Square
无法使用Rectangle
进行 Liskov 替换测试,并且从Rectangle
继承Square
的抽象是不好的。
你们应该查看其他无价的SOLID 原理励志海报 。
Liskov 替换原则(LSP, lsp )是面向对象编程中的一个概念,它指出:
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象。
从本质上讲,LSP 是关于接口和契约以及如何决定何时扩展一个类而不是使用另一个策略(如组合)来实现您的目标。
我所看到的最有效的方式就是Head First OOA&D 。他们提出了一个场景,您是一个项目开发人员,为战略游戏构建框架。
他们提出了一个代表董事会的类,如下所示:
所有方法都将 X 和 Y 坐标作为参数来定位Tiles
的二维数组中的 tile 位置。这将允许游戏开发者在游戏过程中管理棋盘中的单元。
这本书继续改变要求,说游戏框架工作也必须支持 3D 游戏板,以适应有飞行的游戏。因此引入了一个扩展Board
的ThreeDBoard
类。
乍一看,这似乎是一个很好的决定。 Board
提供Height
和Width
属性, ThreeDBoard
提供 Z 轴。
当你看到从Board
继承的所有其他成员时,它崩溃的地方。 AddUnit
, GetTile
, GetUnits
等方法都采用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
的有效替代。