## 连载:面向对象葵花宝典:思想、技巧与实践(32) - LSP原则
LSP是唯一一个以人名命名的设计原则,而且作者还是一个“女博士” ![](https://box.kancloud.cn/2016-01-19_569e21abc5518.gif)
=============================================================
**LSP**,Liskov substitution principle,中文翻译为“**里氏替换原则**”。
这是面向对象原则中唯一一个以人名命名的原则,虽然Liskov在中国的知名度没有UNIX的几位巨匠(Kenneth Thompson、Dennis Ritchie)、GOF四人帮那么响亮,但查一下资料,你会发现其实Liskov也是非常牛的:2008年图灵奖获得者,历史上第一个女性计算机博士学位获得者。其详细资料可以在维基百科上查阅:[http://en.wikipedia.org/wiki/Barbara_Liskov](http://en.wikipedia.org/wiki/Barbara_Liskov)
言归正传,我们来看看LSP原则到底是怎么一回事。
LSP最原始的解释当然来源于Liskov女士了,她在1987年的OOPSLA大会上提出了LSP原则,1988年,她将文章发表在ACM的SIGPLAN Notices杂志上,其中详细解释了LSP原则:
A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra.What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
英文比较长,看起来比较累,我们简单的翻译并归纳一下:
1) **子类的对象提供了父类的所有行为**,且加上子类额外的一些东西(可以是功能,也可以是属性);
2) 当程序基于父类实现时,**如果将子类替换父类而程序不需要修改**,则说明符合LSP原则
虽然我们稍微翻译和整理了一下,但实际上还是很拗口和难以理解。
幸好还有Martin大师也觉得这个不怎么通俗易懂,Robert Martin在1996年为《C++ Reporter》写了一篇题为《The The Liskov Substitution Principle》的文章,解释如下:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻译一下就是:函数使用指向父类的指针或者引用时,必须能够在不知道子类类型的情况下使用子类的对象。
Martin大师解释了一下,相对容易理解多了。但Martin大师还不满足,在2002年,Martin在他出版的《Agile Software Development Principles Patterns and Practices》一书中,又进一步简化为:
Subtypes must be substitutable for their base types。
翻译一下就是:子类必须能替换成它们的父类。
经过Martin大师的两次翻译,我相信LSP原则本身已经解释得比较容易理解了,但问题的关键是:如何满足LSP原则?或者更通俗的讲:什么情况下子类才能替换父类?
我们知道,对于调用者来说(Liskov解释中提到的P),和父类交互无非就是两部分:调用父类的方法、得到父类方法的输出,中间的处理过程,P是无法知道的。
也就是说,调用者和父类之间的联系体现在两方面:函数输入,函数输出。详细如下图:
![](https://box.kancloud.cn/2016-01-20_569f5ccb01a50.jpg)
有了这个图之后,如何做到LSP原则就清晰了:
1) **子类必须实现或者继承父类所有的公有函数**,否则调用者调用了一个父类中有的函数,而子类中没有,运行时就会出错;
2) **子类每个函数的输入参数必须和父类一样**,否则调用父类的代码不能调用子类;
3) **子类每个函数的输出**(返回值、修改全局变量、插入数据库、发送网络数据等)必须不比父类少,否则基于父类的输出做的处理就没法完成。
有了这三条原则后,就可以很方便的判断类设计是否符合LSP原则了。需要注意的是第3条的关键是“不比父类少”,也就是说可以比父类多,即:父类的输出是子类输出的子集。
有的朋友看到这三条原则可能有点纳闷:这三条原则一出,那子类还有什么区别哦,岂不都是一样的实现了,那还会有不同的子类么?
其实如果仔细研究这三条原则,就会发现其中**只是约定了输入输出,而并没有约束中间的处理过程**。例如:同样一个写数据库的输出,A类可以是读取XML数据然后写入数据库,B类可以是从其它数据库读取数据然后本地的数据库,C类可以是通过分析业务日志得到数据然后写入数据库。这3个类的处理过程都不一样,但最后都写入数据到数据库了。
LSP原则最经典的例子就是“长方形和正方形”这个例子。从数学的角度来看,正方形是一种特殊的长方形,但从面向对象的角度来观察,正方形并不能作为长方形的一个子类。原因在于对于长方形来说,设定了宽高后,面积 = 宽 * 高;但对于正方形来说,设定高同时就设定了宽,设定宽就同时设定了高,最后的面积并不是等于我们设定的 宽 * 高,而是等于最后一次设定的宽或者高的平方。
具体代码样例如下:
Rectangle.java
~~~
package com.oo.java.principles.lsp;
/**
* 长方形
*/
public class Rectangle {
protected int _width;
protected int _height;
/**
* 设定宽
* @param width
*/
public void setWidth(int width){
this._width = width;
}
/**
* 设定高
* @param height
*/
public void setHeight(int height){
this._height = height;
}
/**
* 获取面积
* @return
*/
public int getArea(){
return this._width * this._height;
}
}
~~~
Square.java
~~~
package com.oo.java.principles.lsp;
/**
* 正方形
*/
public class Square extends Rectangle {
/**
* 设定“宽”,与长方形不同的是:设定了正方形的宽,同时就设定了正方形的高
*/
public void setWidth(int width){
this._width = width;
this._height = width;
}
/**
* 设定“高”,与长方形不同的是:设定了正方形的高,同时就设定了正方形的宽
*/
public void setHeight(int height){
this._width = height;
this._height = height;
}
}
~~~
UnitTester.java
~~~
package com.oo.java.principles.lsp;
public class UnitTester {
public static void main(String[] args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(4);
rectangle.setHeight(5);
//如下assert判断为true
assert( rectangle.getArea() == 20);
rectangle = new Square();
rectangle.setWidth(4);
rectangle.setHeight(5);
//<span style="color:#ff0000;">如下assert判断为false,断言失败,抛出java.lang.AssertionError</span>
assert( rectangle.getArea() == 20);
}
}
~~~
上面这个样例同时也给出了一个判断子类是否符合LSP的取巧的方法,即:针对父类的单元测试用例,传入子类是否也能够测试通过。如果测试能够通过,则说明符合LSP原则,否则就说明不符合LSP原则
- 前言
- (1) - 程序设计思想的发展
- (2) - 面向对象语言发展历史
- (3) - 面向过程 vs 面向对象
- (4) - 面向对象是瑞士军刀还是一把锤子?
- (5) - 面向对象迷思:面向对象导致性能下降?
- (6) - 不要说你懂“类”
- (7) - “对象”新解
- (8) - “接口” 详解
- (9) - “抽象类” 详解
- (10) - “抽象” 详解
- (11) - “封装” 详解
- (12) - “继承” 详解
- (13) - “多态” 详解
- (14) - 面向对象开发技术流程
- (15) - 需求详解
- (16) - 需求分析终极目的
- (17) - 需求分析518方法
- (18) - 用例分析
- (19) - 功能点提取
- (20) - 用例图的陷阱
- (21) - SSD
- (22) - 领域模型
- (23) - 领域建模三字经
- (24) - 设计模型
- (25) - 类模型
- (26) - 类模型三板斧
- (27) - 动态模型设计
- (28) - 设计原则:内聚&耦合
- (29) - 高内聚低耦合
- (30) - SRP原则
- (31) - OCP原则
- (32) - LSP原则
- (33) - ISP原则
- (34) - DIP原则
- (35) - NOP原则
- (36) - 设计原则如何用?
- (37) - 设计模式:瑞士军刀 or 锤子?
- (38) - 设计模式之道
- (39) - 设计原则 vs 设计模式
- (40) - DECORATOR模式
- (完)- 书籍已经出版