SOLID设计原则:里式替换原则
你好,我是猿java。
前面几篇文章,我们介绍了 SOLID原则的单一职责原则和开闭原则,单一职责描述的模块需要对一类行为负责,开闭原则描述的是对扩展开放,对修改关闭。今天我们就来聊聊SOLID的第三个原则:Liskov替换原则。
什么是里式替换原则?
里式替换原则,Liskov substitution principle(简称LSP),它是以作者 Barbara Liskov(一位美国女性计算机科学家,对编程语言和分布式计算做出了开创性的贡献,于2008年获得图灵奖)的名字命名的,Barbara Liskov 曾在1987年的会议主题演讲“数据抽象”中描述了子类型:
1 | Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T. |
Liskov替换原则的核心:设Φ(x)是关于 T类型对象 x的可证明性质。那么对于 S类型的对象 y,Φ(y)应该为真,其中 S是 T的子类型。
这种科学的定义是不是过于抽象,太烧脑了?因此,在实际软件开发中的 Liskov替换原则可以这样:
1 | The principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. |
该原则定义了在不破坏应用程序的前提下,超类的对象应该可以被其子类的对象替换,这就要求子类对象的行为方式与您的超类对象相同。
Robert C. Martin 对SLP的描述更加直接:
1 | Subtypes must be substitutable for their base types. |
子类型必须可以替代它们的基本类型。
通过上面几个描述,我们可以把 LSP通俗的表达成:子类型必须能够替换其父类。
如何实现Liskov替换原则?
说起 Liskov替换原则的实现,就不得不先看一个著名的违反 LSP设计案例:正放形/长方形问题。尽管这个 case已经有点老掉牙,但是为了帮助理解,我们还是炒一次剩饭。
数学知识告诉我们:正方形是一种特殊的长方形,因此用 java代码分别定义 Rectangle(长方形) 和 Square(正方形)两个类,并且 Square继承 Rectangle,代码如下:
1 | // Rectangle(长方形) |
假设现在的需求是计算几何图形的面积,因此面积计算代码会如下实现:
1 | // 计算面积 |
在这个例子中,Square类重写了 setLength和 setWidth方法,以确保正方形的长度和宽度总是相等的。因此:假设 length=3,width=4
- 对于长方形,面积 = length * width= 3 * 4 = 12,符合预期;
- 然而,用 Square对象替换 Rectangle对象时,程序的行为发生了变化,本期望矩形的面积为12(3 * 4),但实际输出为 4*4=16,违反了里氏替换原则。
如何解决这个 bad case呢?
可以定义一个几何图形的接口,设定一个计算面积的方法,然后长方形、正方形都实现这个接口,实现各自的面积计算逻辑,整体思路如下:
1 | // 基类 |
我们再来看一个 LSP使用的例子:
假设有一个股票交易的场景,而且需要支持债券、股票和期权等不同证券类型的多种交易类型,我们就可以考虑使用 LSP来解决这个问题。
首先,我们定义一个交易的基类,并且在基类中定义买入和卖出两个方法实现,代码如下:
1 | // 定义一个交易类 |
接着,定义一个子类:股票交易,它和基类具有相同的买入和卖出行为,因此,在股票交易子类中需要重写基类的方法,代码如下:
1 | // 定义股票交易子类,定义股票特定的买卖动作逻辑 |
同样,定义一个子类:基金交易,它和基类具有相同的买入和卖出行为,因此,在基金交易子类中需要重写基类的方法,代码如下:
1 | // 定义基金交易子类,定义基金特定的买卖动作逻辑 |
同样,我们还可以定义了债券交易子类,债券交易和交易基类具有相同的行为:买入和卖出。所以只需要重写基类的方法,实现子类特定的实现就ok了。
1 | // 定义债券交易子类,定义债券特定的买卖动作逻辑 |
上述交易的案例,股票交易和基金交易子类替换基类之后,并没有破坏基类的买入卖出行为,更具体地说,替换的子类实例仍提供 buy()和 sell(),可以以相同方式调用的功能。这个符合LSP。
经过我们的抽象、分离和改造之后,Stock.updateStock()类就稳定下来了,再也不需要增加一个事件然后增加一个else if分支处理。这种抽象带来的好处也是很明显的:每次有新的库存变更事件,只需要增加一个实现类,其他的逻辑都不需要更改,当库存事件无效时只需要把实现类删除即可。
总结
Liskov替换原则扩展了OCP开闭原则,它描述的子类型必须能够替换其父类型,而不会破坏应用程序。因此,子类需要遵循以下规则:
- 不要对输入参数实施比父类实施更严格的验证规则。
- 至少对父类应用的所有输出参数应用相同的规则。
Liskov替换原则相对前面的单一职责和开闭原则稍微晦涩一些,因此在开发中容易误用,因此我们特别要注意类之间是否存在继承关系。
LSP不仅可以用在类关系上,也可以应用在接口设计中。
学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。