设计原则:里式替换原则(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年提出,所以使用的是这位女博士的性命名的一个 ...
随机推荐
- Java中print、printf、println的区别
Java中print.printf.println的区别 区别 print:标准输出,但不换行,不可以空参: println:标准输出,但会自动换行,可以空参,可以看做:println()相当于pri ...
- iOS 兼容性处理
1. scroll滑动层,在iOS中滑动不流畅的处理 -webkit-overflow-scrolling:touch; //在滑动层标签添加这个样式 2. iOS 系统中input标签,去掉圆角效果 ...
- Java内存模型(JMM)是什么?JMM 通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证
Java内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范. Java内存模型是根据英文Java Memo ...
- docker封装vue项目并使用jenkins发布
一.概述 vue项目可以打一个dist静态资源包,直接使用Nginx发布即可. 现在由于要上docker,需要将vue项目和nginx打成一个镜像才行. 项目结构如下: ./ ├── build │ ...
- AXU2CGB开发板验证Vitis加速基本平台创建
Vitis 加速基本平台创建 1.Vivado 工程创建,硬件平台bd 图如下所示 1.1.双击Block图中ZYNQ核,配置相关参数 1.1.1.Low Speed 配置,在 I/O Configu ...
- drf给上传图片重命名
1.先在你项目中添加一个文件夹如:system 在文件夹下添加__init__.py 和storage.py文件,并在storage.py中添加如下代码: #复制代码 -- coding: UTF-8 ...
- win8 下删除服务
1.右键 我的电脑-管理-服务和应用程序-服务,找到想要删除的服务,双击后复制服务名称. 2.管理员身份运行cmd 在命令框中输入 sc delete "secbizsrv" 就删 ...
- 如何对shell脚本中斜杠进行转义?
1.在编写shell脚本时,经常会遇到对某个路径进行替换,而路径中包含斜杠(/),此时我们就需要对路径中涉及的斜杠进行转义,否则执行失败.具体示例如下: 需求描述: 将sjk目录下的test文件中的p ...
- 安全计算环境之剩余信息保护-windows
参考https://blog.csdn.net/ubjewen/article/details/107587951 应保证鉴别信息所在的存储空间被释放或重新分配前得到完全清除 交互式登录: 之前登录到 ...
- VUE移动端音乐APP学习【四】:scroll组件及loading组件开发
scroll组件 制作scroll 组件,然后嵌套一个 DOM 节点,使得该节点就能够滚动.该组件中需要引入 BetterScroll 插件. scroll.vue: <template> ...