系列文章

设计原则:单一职责(SRP)

设计原则:开闭原则(OCP)

设计原则:里式替换原则(LSP)

设计原则:接口隔离原则(ISP)

设计原则:依赖倒置原则(DIP)

何谓高质量代码?

理解RESTful API

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 {@Overridepublic void set(String key,String value){// 使用Redis}
}public class Memcache extends Cache {@Overridepublic void set(String key,String value){// 使用Memcache}
}class CacheTest {@Testpublic 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){// 使用Redisif(key==null||key.length<10){throw new IllegalArgumentException("key长度不能小于10");}    }
}class CacheTest {@Testpublic void set() {Cache cache = new Cache();assertTrue(cache.set("testKey", "testValue"));cache = new Redis();assertTrue(cache.set("testKey", "testValue"));}
}

此时如果我们在使用父类对象的时候替换成子类对象,那set方法就会有异常抛出。程序的逻辑行为就发生了变化,虽然改造之后的代码仍然可以通过子类来替换父类 ,但是,从设计思路上来讲,Redis子类的设计是不符合里式替换原则的。

继承和多态是面向对象语言所提供的一种语法,一种代码实现的思路,而里式替换则是一种思想,是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

2 规则

其实历史替换原则的核心就是“约定”,父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能,对输入、输出、异常的约定,甚至包括注释中一些特殊说明等。

2.1 子类方法不能违背父类方法对输入输出异常的约定

  1. 前置条件不能被加强

    前置条件即输入参数是不能被加强的,就像上面Cache的示例,Redis子类对输入参数Key的要求进行了加强,此时在调用处替换父类对象为子类对象就可能引发异常。

    也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。

  2. 后置条件不能被削弱

    后置条件即输出,假设我们的父类方法约定输出参数要大于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    }
  3. 不能违背对异常的约定

    在父类中,某个函数约定,只会抛出 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 小结

面向对象的编程思想中提供了继承和多态是我们可以很好的实现代码的复用性和可扩展性,但继承并非没有缺点,因为继承的本身就是具有侵入性的,如果使用不当就会大大增加代码的耦合性,而降低代码的灵活性,增加我们的维护成本,然而在实际使用过程中却往往会出现滥用继承的现象,而里式替换原则可以很好的帮助我们在继承关系中进行父子类的设计。

系列文章

设计原则:单一职责(SRP)

设计原则:开闭原则(OCP)

设计原则:里式替换原则(LSP)

设计原则:接口隔离原则(ISP)

设计原则:依赖倒置原则(DIP)

何谓高质量代码?

理解RESTful API

关注下方公众号,回复“代码的艺术”,可免费获取重构、设计模式、代码整洁之道等提升代码质量等相关学习资料

设计原则:里式替换原则(LSP)相关推荐

  1. 面向对象程序设计原则——里式替换原则

    详细分析请查看原文出处 详细分析请查看原文出处 详细分析请查看原文出处 文章目录 @[toc] 定义 意义 做法 实践 uml图 代码部分 定义 Liskov于1987年提出了一个关于继承的原则&qu ...

  2. LSP 里式替换原则 c# 1614092345

    LSP 里式替换原则 子类对象可以替换所有使用的父类对象 且程序行为没有变化 简单的说: 父类的方法,子类都可以使用 直接使用子类,可以同时获取父类的方法与自有的方法,应用的泛围更广

  3. 论重写和里式替换原则(LSP)

    对于重写的原则,很多人总是巴拉巴拉一大堆两同两小一大,记不住不说,还不明白为啥,搞得花里胡哨. 其实万事万物的结果自然有其原理,JAVA作为一门编程语言,其更是有严格的语言规范和简洁性要求. 那为啥重 ...

  4. 里式替换(LSP)跟多态有何区别?

    在上两节课中,我们学习了 SOLID 原则中的单一职责原则和开闭原则,这两个原则都比较重要,想要灵活应用也比较难,需要你在实践中多加练习.多加体会.今天,我们再来学习 SOLID 中的"L& ...

  5. 设计模式-02.经典设计原则-第一节-单一职责原则,开闭原则,里式替换,接口隔离【万字长文系列】

    文章目录 设计模式经典设计原则-第一节 单一职责原则(SRP) 如何理解单一职责原则? 如何判断类的职责是否足够单一? 类的职责是否设计得越单一越好? 开闭原则(OCP) 如何理解"对扩展开 ...

  6. java里氏替换原则例子_java 设计原则(六)里氏替换原则

    定义:如果对每一个类型T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型. 定义扩展:一个软件 ...

  7. 软件设计原则之里氏替换原则、依赖倒置原则

    系列文章目录 软件设计原则之单一职责原则.开闭原则 软件设计原则之里氏替换原则.依赖倒置原则 软件设计原则之接口隔离原则.合成复用原则.迪米特原则 文章目录 系列文章目录 一.里氏替换原则 什么是里氏 ...

  8. 七大设计原则之里氏替换原则应用

    目录 1 里氏替换原则 2 里氏替换原则应用 1 里氏替换原则 里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为 T1 的对象 o1,都有类型为 ...

  9. 6大设计原则之里氏替换原则

    面对对象中的继承 优点如下: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性 提高代码的重用性 子类可以形如父类,但又异于父类 提高代码的可扩展性,很多开源框架的扩展接口都是通过继承父类 ...

最新文章

  1. lvs后端realserver的vip管理脚本lvs-realsvr.sh
  2. Java 多线程编程(锁优化)
  3. 双链表(插入节点操作)
  4. 一些C++的开源项目和C++库以及修炼C++的方法
  5. tf.split()
  6. 递归算法——汉诺塔问题
  7. springMVC各种注解及解释和使用
  8. Python——逻辑运算(or,and)
  9. 树形结构 —— 并查集 —— 带权并查集
  10. 嫌多(线程/进程)太慢? 嫌Scrapy太麻烦?没事,异步高调走起!——瓜子二手车
  11. 一路风景---我期待的是师生关系
  12. VB:您知道 Mid$ 函量可以放在 '=' 的左方吗
  13. 解决QML Window 增加radius效果
  14. 计算机网络实用技术教程txt,计算机网络实用技术教程
  15. Mybatis:高级知识1- resultMap实现一对一、一对多、多对多
  16. Python每输出n个换行
  17. 低级程序员和高级程序员的区别,难道这就是最强王者的世界吗?
  18. miui11稳定版获取完整root_MIUI11系统怎么样刷入开发版获得Root超级权限
  19. K3实现按虚拟件/组件发料
  20. win10中文输入法添加“美式键盘”布局

热门文章

  1. 获取jar包中的文件,及遍历jar包中的文件
  2. 四种连接类型:inner(内连接),left[outer](左外连接),right[outer](右外连接),full[outer](完全外连接)
  3. 【linux】linux终端命令总结
  4. Jmeter——BeanShell PreProcessor的用法
  5. python空类型用什么表示_python空集合如何表示
  6. zcpa matlab,matlab习题结果解析.doc
  7. html浪漫恋爱情侣表白网站模板-html创意情侣表白网站整站源码
  8. 使用Godaddy的API批量修改域名的NameServers,指向CloudFlare的NS,享受免费的抗DDOS保护!
  9. 解决Ubuntu16.04耳机没声音问题
  10. insmod 加载模块过程、modprobe 与 insmod的区别 、lsmod命令