连载:面向对象葵花宝典:思想、技巧与实践(32) - LSP原则

简介:

LSP是唯一一个以人名命名的设计原则,而且作者还是一个“女博士” 大笑

=============================================================


LSP,Liskov substitution principle,中文翻译为“里氏替换原则”。

 

这是面向对象原则中唯一一个以人名命名的原则,虽然Liskov在中国的知名度没有UNIX的几位巨匠(Kenneth Thompson、Dennis Ritchie)、GOF四人帮那么响亮,但查一下资料,你会发现其实Liskov也是非常牛的:2008年图灵奖获得者,历史上第一个女性计算机博士学位获得者。其详细资料可以在维基百科上查阅: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是无法知道的。

 

也就是说,调用者和父类之间的联系体现在两方面:函数输入,函数输出。详细如下图:

 

有了这个图之后,如何做到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);
        
        //如下assert判断为false,断言失败,抛出java.lang.AssertionError
        assert( rectangle.getArea() == 20);
    }
}

上面这个样例同时也给出了一个判断子类是否符合LSP的取巧的方法,即:针对父类的单元测试用例,传入子类是否也能够测试通过。如果测试能够通过,则说明符合LSP原则,否则就说明不符合LSP原则


================================================ 
转载请注明出处:http://blog.csdn.net/yunhua_lee/article/details/26807601
================================================ 


相关文章
|
测试技术 UED Python
App自动化测试:高级控件交互技巧
Appium 的 Actions 类支持在移动应用自动化测试中模拟用户手势,如滑动、长按等,增强交互性测试。ActionChains 是 Selenium 的概念,用于网页交互,而 Actions 专注于移动端。在Python中,通过ActionChains和W3C Actions可以定义手势路径,例如在手势解锁场景中,先点击设置,然后定义触点移动路径执行滑动解锁,最后验证解锁后的元素状态。此功能对于确保应用在复杂交互下的稳定性至关重要。
|
数据采集 数据挖掘 API
主流电商平台数据采集API接口|【Python爬虫+数据分析】采集电商平台数据信息采集
随着电商平台的兴起,越来越多的人开始在网上购物。而对于电商平台来说,商品信息、价格、评论等数据是非常重要的。因此,抓取电商平台的商品信息、价格、评论等数据成为了一项非常有价值的工作。本文将介绍如何使用Python编写爬虫程序,抓取电商平台的商品信息、价格、评论等数据。 当然,如果是电商企业,跨境电商企业,ERP系统搭建,我们经常需要采集的平台多,数据量大,要求数据稳定供应,有并发需求,那就需要通过接入电商API数据采集接口,封装好的数据采集接口更方便稳定高效数据采集。
|
存储 数据采集 NoSQL
收藏!一张图帮你快速建立大数据知识体系
对海量数据进行存储、计算、分析、挖掘处理需要依赖一系列的大数据技术,而大数据技术又涉及了分布式计算、高并发处理、高可用处理、集群、实时性计算等,可以说是汇集了当前 IT 领域热门流行的 IT 技术。本文对大数据技术知识体系进行划分,共分为基础技术、数据采集、数据传输、数据组织集成、数据应用、数据治理,进行相关的阐述说明,并列出目前业界主流的相关框架、系统、数据库、工具等。(文末福利:下载大数据知识体系图)
18036 3
收藏!一张图帮你快速建立大数据知识体系
|
弹性计算 调度 容器
在Kubernetes集群中通过LocalVolume Provisioner使用本地盘
介绍 阿里云在部分ECS类型中提供了本地盘配置,本地盘具有低时延、高随机IOPS、高吞吐量和高性价比的优势,在一些对性能要求很高的应用中有很大优势。 在Kubernetes系统中使用本地盘可以通过HostPath、LocalVolume等类型的PV使用: HostPath: 卷本身不带有调度信息,如果想对每个pod固定在某个节点上,就需要对pod配置nodeSelector等调度信息; LocalVolume: 卷本身包含了调度信息,使用这个卷的pod会被固定在特定的节点上,这样可以很好的保证数据的连续性。
6128 0
|
数据库 数据安全/隐私保护
国产化DM达梦数据库 - 用户状态查询、锁定与解锁,“登录失败次数超过限制”问题解决
国产化DM达梦数据库 - 用户状态查询、锁定与解锁,“登录失败次数超过限制”问题解决
2813 0
国产化DM达梦数据库 - 用户状态查询、锁定与解锁,“登录失败次数超过限制”问题解决
|
JavaScript 前端开发 安全
80 行 JS 代码实现页面添加水印:文字水印、多行文字水印、图片水印、文字&图片水印
80 行 JS 代码实现页面添加水印:文字水印、多行文字水印、图片水印、文字&图片水印 1. 信息标识: 水印可以用于标识文档的所有者、保密级别、状态或其他相关信息,帮助用户更好地理解文档内容的属性。 2. 版权保护: 在文档中添加水印可以帮助保护内容的版权,防止他人未经授权地复制、转载或篡改内容。 3. 安全保护: 对于敏感信息或机密文档,添加水印可以帮助防止信息泄露,提高文档的安全性。 4. 提升专业性: 在一些场景下,如商业报告、合同文件等,添加水印可以增加文档的专业性和正式性。 5. 防止截屏或拷贝: 在网页中添加水印可以防止用户通过截屏或复制粘贴等方式非法获取文档内容。
407 1
80 行 JS 代码实现页面添加水印:文字水印、多行文字水印、图片水印、文字&图片水印
|
存储 算法 C++
C++一分钟之-容器概览:vector, list, deque
【6月更文挑战第21天】STL中的`vector`是动态数组,适合随机访问,但插入删除非末尾元素较慢;`list`是双向链表,插入删除快但随机访问效率低;`deque`结合两者优点,支持快速双端操作。选择容器要考虑操作频率、内存占用和性能需求。注意预分配容量以减少`vector`的内存重分配,使用迭代器而非索引操作`list`,并利用`deque`的两端优势。理解容器内部机制和应用场景是优化C++程序的关键。
328 5
|
SQL 人工智能 SEO
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
682 2
|
存储 缓存 算法
【Conan 入门教程 】了解 Conan2.1 中默认生成器的作用
【Conan 入门教程 】了解 Conan2.1 中默认生成器的作用
352 1