系列文章

设计原则:单一职责(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 {
@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 子类方法不能违背父类方法对输入输出异常的约定

  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. java6大原则之单一职责原则,里式替换原则

    单一职责原则:一个接口,一个类,一个方法,最好只做一类事,当然,在真实的项目中,一系列因素下,很难做到单一职责原则,但是针对接口是可以做到的,方法和类要尽量做到 里式替换原则:父类出现的地方,换成子类 ...

  2. 设计模式学习--面向对象的5条设计原则之Liskov替换原则--LSP

    一.LSP简介(LSP--Liskov Substitution Principle): 定义:如果对于类型S的每一个对象o1,都有一个类型T的对象o2,使对于任意用类型T定义的程序P,将o2替换为o ...

  3. IOS设计模式的六大设计原则之里氏替换原则(LSP,Liskov Substitution Principle)

    定义 里氏替换原则的定义有两种,据说是由麻省理工的一位姓里的女士所提出,因此以其名进行命名. 定义1:如果对一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1所定义的程序P中在o1全都替换 ...

  4. 深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP

    前言 本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle ). 英文原文:http ...

  5. Java设计原则之里氏替换原则

    里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出.其严格表述如下:如果对每一个类型为S的 ...

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

    1.什么是开闭原则 开闭原则的英文是Open Closed Principle,缩写就是OCP.其定义如下: 软件实体(模块.类.方法等)应该"对扩展开放.对修改关闭". 从定义上 ...

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

    接口隔离原则的英文是Interface Segregation Principle,缩写就是ISP.与里氏替换原则一样其定义同样有两种 定义1: Clients should not be force ...

  8. 【C#设计模式】里氏替换原则

    今天,我们再来学习 SOLID 中的"L"对应的原则:里式替换原则. 里氏替换原则 里氏替换原则(Liskov Substitution Principle):派生类(子类)对象能 ...

  9. 【面向对象设计原则】之里氏替换原则(LSP)

    里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing 教授于1994年提出,所以使用的是这位女博士的性命名的一个 ...

随机推荐

  1. idea更改包名无法加载主类解决

    把工程下面的.idea目录下的workspace.xml里面的路径改成你最新的路径即可. <option name="SPRING_BOOT_MAIN_CLASS" valu ...

  2. 【函数分享】每日PHP函数分享(2021-3-1)

    array_filter - 使用回调函数过滤数组的元素 说明 array_filter ( array $array , callable|null $callback = null , int $ ...

  3. 女朋友看了会生气的回答 URI和URL有什么区别?

    URL是什么 URL 代表着是统一资源定位符(Uniform Resource Locator).作用是为了告诉使用者 某个资源在 Web 上的地址.这个资源可以是一个 HTML 页面,一个 CSS ...

  4. 别再恐惧 IP 协议(万字长文 | 多图预警)

    尽人事,听天命.博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 「CS-Wiki」Gitee ...

  5. 【DB宝42】MySQL高可用架构MHA+ProxySQL实现读写分离和负载均衡

    目录 一.MHA+ProxySQL架构 二.快速搭建MHA环境 2.1 下载MHA镜像 2.2 编辑yml文件,创建MHA相关容器 2.3 安装docker-compose软件(若已安装,可忽略) 2 ...

  6. WebRTC 音视频同步原理与实现

    所有的基于网络传输的音视频采集播放系统都会存在音视频同步的问题,作为现代互联网实时音视频通信系统的代表,WebRTC 也不例外.本文将对音视频同步的原理以及 WebRTC 的实现做深入分析. 时间戳 ...

  7. golang——net/rpc/jsonrpc包学习

    1.jsonrpc包 该实现了JSON-RPC的ClientCodec和ServerCodec接口,可用于rpc包. 可用于跨语言使用go rpc服务. 2.常用方法 (1)func Dial(net ...

  8. locust工具使用详解

    今年负责部门的人员培养工作,最近在部门内部分享和讲解了locust这个工具,今天再博客园记录下培训细节 一.简介 1.优势 locust是python语言开发的一款的开源的的性能测试框架,他比jmet ...

  9. 学员和教师管理优化用例点整理v2.0

    更新记录: 更新内容 更新人 更新时间 新建 Young 2021.01.08 12:06 彭洋洋确认结果疑问 Young 2021.01.08 15:06 问题集锦 1. 购买成功页点击完成返回路径 ...

  10. python程序控制结构

    一:顺序结构 顺序结构的程序是指程序中的所有语句都是按照书写顺序逐一执行的,但是顺序结构的程序功能有限. 二:选择结构 选择结构也称为分支结构,用于处理在程序中出现两条或更多执行路径可供选择的情况.选 ...