软件架构-里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计的基本原则之一。

里氏替换原则认为:

任何基类(父类或接口)可以出现的地方,子类(实现类)一定可以出现。 LSP是继承复用的基石,只有当子类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而子类也能够在基类的基础上增加新的行为

为了更好理解这个原则,我们举两个例子(一个违反原则案例、一个符合原则案例)。

 

违反原则案例:长方形和正方形

从长方形和正方形的特性中,我们知道正方形也是一种特殊的长方形。现在有一个需求是通过指定长方形(或正方形)的长和宽,求得该长方形的面积。由于正方形是一种特殊的长方形,开发者设计时把长方形作为父类,正方形作为子类并继承父类,如下图:

在这个案例中,Square类不应该是Rectangle类的子类,因为Rectangle类的高和宽可以分别修改,而Square类的高和宽则必须一同修改。由于User类直接对接和操作Rectangle类,因此会带来一些混淆。如下代码:

如果变量 r 指向的是Rectangle类对象,此断言是成立的,但是如果变量 r 指向的是Square类对象,则这个断言结果是不成立的。

想要避免这种违反LSP的行为,有一个方法是在User类中增加用于区分Rectangle和Square的检测逻辑(例如增加 if 判断逻辑)。但是这样一来,User类的行为又将依赖于它所使用的类,这两个类就不能互相替换了。

 

符合原则案例:排序算法

假设现在有一个需求,要求对一组集合的数据进行排序。我们定义了一个Sort接口,该接口有一个sort()排序方法,最开始我们使用了冒泡排序,所以定义了一个Sort接口的实现类BubbleSort。

集合数据比较少时,冒泡排序的处理速度还可以接受,但是随着集合数据的增加,冒泡排序的处理效率下降越明显。之后,我们打算使用快速排序替换掉冒泡排序,所以增加了一个排序实现类QuickSort,而User类直接操作的是Sort接口,因此User类不用做任何修改。如下图:

上述设计是符合LSP原则的,因为User类的行为并不依赖于Sort接口的任何一个实现类。如果由于需求需要,要使用堆排序替换掉快速排序,只需要增加一个排序实现类HeapSort即可。也就是说,这3个子类(实现类)的对象都是可以用来替换Sort基类(接口)对象的。

 

总结

LSP是一种更广泛的、指导接口与其实现方式的设计原则。这里提到的接口可以有多种形式——可以是Java风格的接口,具有多个实现类;也可以像C++一样,父类指针指向子类对象;甚至可以是一个REST接口,多个服务程序只要遵循该REST的规范并响应同一个REST接口。

LSP应该被应用于软件架构层面,因为一旦违背了可替换性,该系统架构就不得不为此添加大量复杂的应对机制。

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年10月15日
下一篇 2020年10月15日

相关推荐