设计原则:里式替换原则(LSP)
系列文章
1 定义
里氏原则的英文是Open Closed Principle,缩写就是OCP。其定义有两种
定义1:
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
(如果S是T的子类型,则类型T的对象可以替换为类型S的对象,而不会破坏程序。)
定义2:
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
(所有引用其父类对象方法的地方,都可以透明的使用其子类对象)
这两种定义方式其实都是一个意思,即:应用程序中任何父类对象出现的地方,我们都可以用其子类的对象来替换,并且可以保证原有程序的逻辑行为和正确性。
如何理解里氏替换与继承多态
很多人(包括我自己)乍一看,总觉得这个原则和继承多态的思想差不多。但其实里氏替换和继承多态有关系,但并不是一回事,我们可以通过一个例子来看一下
public class Cache {
public void set(String key,String value){
// 使用内存Cache
}
}
public class Redis extends Cache {
@Override
public void set(String key,String value){
// 使用Redis
}
}
public class Memcache extends Cache {
@Override
public void set(String key,String value){
// 使用Memcache
}
}
class CacheTest {
@Test
public void set() {
Cache cache = new Cache();
assertTrue(cache.set("testKey", "testValue"));
cache = new Redis();
assertTrue(cache.set("testKey", "testValue"));
cache = new Memcache();
assertTrue(cache.set("testKey", "testValue"));
}
}
我们定义了一个Cache类来实现程序中写缓存的逻辑,它有两个子类Redis和Memcache来实现不同的缓存工具,看到这个例子很多人可能会有疑问这不就是利用了继承和多态的思想吗?
不错,的确是这样的,而且在这个例子中两个子类的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
但如果这时我们需要对Redis子类方法中增加对Key长度的验证。
public class Redis extends Cache {
public void set(String key,String value){
// 使用Redis
if(key==null||key.length<10){
throw new IllegalArgumentException("key长度不能小于10");
}
}
}
class CacheTest {
@Test
public void set() {
Cache cache = new Cache();
assertTrue(cache.set("testKey", "testValue"));
cache = new Redis();
assertTrue(cache.set("testKey", "testValue"));
}
}
此时如果我们在使用父类对象的时候替换成子类对象,那set方法就会有异常抛出。程序的逻辑行为就发生了变化,虽然改造之后的代码仍然可以通过子类来替换父类 ,但是,从设计思路上来讲,Redis子类的设计是不符合里式替换原则的。
继承和多态是面向对象语言所提供的一种语法,一种代码实现的思路,而里式替换则是一种思想,是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
2 规则
其实历史替换原则的核心就是“约定”,父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能,对输入、输出、异常的约定,甚至包括注释中一些特殊说明等。
2.1 子类方法不能违背父类方法对输入输出异常的约定
前置条件不能被加强
前置条件即输入参数是不能被加强的,就像上面Cache的示例,Redis子类对输入参数Key的要求进行了加强,此时在调用处替换父类对象为子类对象就可能引发异常。
也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
后置条件不能被削弱
后置条件即输出,假设我们的父类方法约定输出参数要大于0,调用父类方法的程序根据约定对输出参数进行了大于0的验证。而子类在实现的时候却输出了小于等于0的值。此时子类的涉及就违背了里氏替换原则
public void calculatePrice()
{
Strategy strategy= new Strategy();
BigDecimal price= strategy.getPrice();
if (price <= Decimal.Zero)
{
throw new ArgumentOutOfRangeException("price", "price must be positive and non-zero");
}
// do something }不能违背对异常的约定
在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
2.2 子类方法不能违背父类方法定义的功能
public class Product {
private BigDecimal amount;
private Calendar createTime;
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public Calendar getCreateTime() {
return createTime;
}
public void setCreateTime(Calendar createTime) {
this.createTime = createTime;
}
}
public class ProductSort extends Sort<Product> {
public void sortByAmount(List<Product> list) {
//根据时间进行排序
list.sort((h1, h2)->h1.getCreateTime().compareTo(h2.getCreateTime()));
}
}
父类中提供的 sortByAmount() 排序函数,是按照金额从小到大来进行排序的,而子类重写这个 sortByAmount() 排序函数之后,却是是按照创建日期来进行排序的。那子类的设计就违背里式替换原则。
实际上对于如何验证子类设计是否符合里氏替换原则其实有一个小技巧,那就是你可以使用父类的单测来运行子类的代码,如果不可以正常运行,那么你就要考虑一下自己的设计是否合理了!
2.3 子类必须完全实现父类的抽象方法
如果你设计的子类不能完全实现父类的抽象方法那么你的设计就不满足里式替换原则。
// 定义抽象类枪
public abstract class AbstractGun{
// 射击
public abstract void shoot();
// 杀人
public abstract void kill();
}
比如我们定义了一个抽象的枪类,可以射击,也可以杀人。无论是步枪还是手枪都可以射击和啥人,我们可以定义子类来继承这个父类
// 定义手枪,步枪,机枪
public class Handgun extends AbstractGun{
public void shoot(){
// 手枪射击
}
public void kill(){
// 手枪杀人
}
}
public class Rifle extends AbstractGun{
public void shoot(){
// 步枪射击
}
public void kill(){
// 步枪杀人
}
}
但是如果我们在这个继承体系内加入一个玩具枪,就会有问题了,因为玩具枪只能射击,不能杀人。但是我经常看到很多人写代码会有这种套路。
public class ToyGun extends AbstractGun{
public void shoot(){
// 玩具枪射击
}
public void kill(){
// 因为玩具枪不能杀人,就返回空,或者直接throw一个异常出去
throw new Exception("我是个玩具枪,惊不惊喜,意不意外,刺不刺激?");
}
}
这时,我们如果把使用父类对象的地方替换为子类对象,显然是会有问题的(士兵上战场结果发现自己拿的是个玩具)。
而这种情况不仅仅不满足里氏替换原则,也不满足接口隔离原则,对于这种场景可以通过接口隔离+委托的方式来解决。
3 小结
面向对象的编程思想中提供了继承和多态是我们可以很好的实现代码的复用性和可扩展性,但继承并非没有缺点,因为继承的本身就是具有侵入性的,如果使用不当就会大大增加代码的耦合性,而降低代码的灵活性,增加我们的维护成本,然而在实际使用过程中却往往会出现滥用继承的现象,而里式替换原则可以很好的帮助我们在继承关系中进行父子类的设计。
系列文章
关注下方公众号,回复“代码的艺术”,可免费获取重构、设计模式、代码整洁之道等提升代码质量等相关学习资料
设计原则:里式替换原则(LSP)的更多相关文章
- java6大原则之单一职责原则,里式替换原则
单一职责原则:一个接口,一个类,一个方法,最好只做一类事,当然,在真实的项目中,一系列因素下,很难做到单一职责原则,但是针对接口是可以做到的,方法和类要尽量做到 里式替换原则:父类出现的地方,换成子类 ...
- 设计模式学习--面向对象的5条设计原则之Liskov替换原则--LSP
一.LSP简介(LSP--Liskov Substitution Principle): 定义:如果对于类型S的每一个对象o1,都有一个类型T的对象o2,使对于任意用类型T定义的程序P,将o2替换为o ...
- IOS设计模式的六大设计原则之里氏替换原则(LSP,Liskov Substitution Principle)
定义 里氏替换原则的定义有两种,据说是由麻省理工的一位姓里的女士所提出,因此以其名进行命名. 定义1:如果对一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1所定义的程序P中在o1全都替换 ...
- 深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP
前言 本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle ). 英文原文:http ...
- Java设计原则之里氏替换原则
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出.其严格表述如下:如果对每一个类型为S的 ...
- 设计原则:开闭原则(OCP)
1.什么是开闭原则 开闭原则的英文是Open Closed Principle,缩写就是OCP.其定义如下: 软件实体(模块.类.方法等)应该"对扩展开放.对修改关闭". 从定义上 ...
- 设计原则:接口隔离原则(ISP)
接口隔离原则的英文是Interface Segregation Principle,缩写就是ISP.与里氏替换原则一样其定义同样有两种 定义1: Clients should not be force ...
- 【C#设计模式】里氏替换原则
今天,我们再来学习 SOLID 中的"L"对应的原则:里式替换原则. 里氏替换原则 里氏替换原则(Liskov Substitution Principle):派生类(子类)对象能 ...
- 【面向对象设计原则】之里氏替换原则(LSP)
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing 教授于1994年提出,所以使用的是这位女博士的性命名的一个 ...
随机推荐
- 用注解开发SpringMVC
Spring2.5以后,用注解开发SpringMVC的功能十分强大,注解也是SpringMVC的精髓.在实际开发中,都会使用注解来实现. 这让SpringMVC开发工作量最小化,开发者只要专注于业务逻 ...
- Mybites逆向工程的搭建
这个链接写的很全:https://www.cnblogs.com/whgk/p/7140638.html 这段时间太忙,等周末写上自己尝试的步骤.暂时仅作记录.
- 为什么ConcurrentHashMap,HashTable不支持key,value为null?
ConcurrentHashmap.HashMap和Hashtable都是key-value存储结构,但他们有一个不同点是 ConcurrentHashmap.Hashtable不支持key或者val ...
- HashMap是如何进行扩容的?
HashMap通过resize()方法进行扩容. 源码解析: resize()函数有两种使用情况: 一.当table数组为null时初始化hash表. 二.当table数组不为null时进行扩容. 1 ...
- LinkedHashMap转对象
当需要把map转为实体对象时,需要结合fastjson来实现,方法如下: LinkedHashMap<String,Object> hashMap = new LinkedHashMap& ...
- 安鸾渗透实战平台-PHP代码练习
0x00 前言 --工欲善其事,必先利其器 0x01 代码理解 (1)linux命令 pwd 查看当前工作目录的完整路径 cd / 切换到根目录 ls / 查看根目录下的所有目录和文件 cat /[f ...
- JavaScript 模拟 sleep
用 JS 实现沉睡几秒后再执行,有好几种方式,但都不完美,以下是我感觉比较好的一种方式 function sleep(time) { return new Promise((resolve) => ...
- 关于Laravel框架中Guard的底层实现
1. 什么是Guard 在Laravel/Lumen框架中,用户的登录/注册的认证基本都已经封装好了,开箱即用.而登录/注册认证的核心就是: 用户的注册信息存入数据库(登记) 从数据库中读取数据和用户 ...
- 【Azure 云服务】Azure Cloud Service在发布新部署后遇见不能RDP(远程连接)到实例时如何处理?
Azure 云服务是PaaS 的一个示例. 与 Azure 应用服务一样,此技术设计用于支持可缩放.可靠且运营成本低廉的应用程序. 同样,应用服务托管在虚拟机 (VM) 上,Azure 云服务也是如此 ...
- Shell脚本控制docker容器启动顺序
1.遇到的问题 在分布式项目部署的过程中,经常要求服务器重启之后,应用(包括数据库)能够自动恢复使用.虽然使用docker update --restart=always containerid能够让 ...