里氏替换原则LSP(Liskov Subsituation Principle)

里氏替换原则定义

所有父类出现的地方可以使用子类替换并不会出现错误或异常,但是反之子类出现的地方不一定能用父类替换。

LSP的四层含义

  • 子类必须完全实现父类的方法
  • 子类可以自己的个性(属性和方法)
  • 覆盖或实现父类的方法时输入参数可以被放大
  • 覆盖或实现父类的方法时输出结果可以被缩小

LSP的定义含义1——子类必须完全实现父类的方法

假设如下场景:定义一个枪支抽象类,一个场景类,三个枪支实现类,一个士兵类。此处,三个枪支完全实现了父类的方法。

关联关系:实线箭头

泛化关系:实线空心箭头(继承关系)

依赖关系:虚线箭头(使用关系)一个类需要另一个类的协助



抽象枪支类:射击功能

package des.lsp;

/**
* 抽象类 枪支
*/
abstract class AbstractGun {
//射击功能
public abstract void shoot();
}

子类实现

package des.lsp;

/**
* 手枪
*/
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.print("手枪可以射击");
}
}
package des.lsp;

/**
* 手枪
*/
public class MachineGun extends AbstractGun {
@Override
public void shoot() {
System.out.print("步枪可以射击");
}
}
package des.lsp;

/**
* 步枪
*/
public class Rifle extends AbstractGun {
@Override
public void shoot() {
System.out.print("步枪可以射击");
}
}

士兵类:士兵类使用的是抽象枪支类,具体的需要在场景类中指定。

类中调用其他类必须使用父类或接口,若不能使用则其实已经违背了LSP原则。

package des.lsp;

public class Soldier {
private AbstractGun gun;
public void setGun(AbstractGun _gun){
this.gun = _gun;
};
public void killEnemy(){
System.out.print("士兵开始杀人...");
gun.shoot();
} }

场景类

package des.lsp;

public class Client {
public static void main(String[] args) {
// write your code here
Soldier s = new Soldier();
s.setGun(new Rifle());
s.killEnemy();
}
}

如果加入一个玩具枪类,即玩具枪类同样继承抽象枪支类,此时就会存在子类不能实现枪支类方法的情况,因为玩具枪和枪最本质的区别是玩具枪不能射击的,是无法杀死人的。但是,玩具枪的其他属性,比如颜色等一些属性可以委托抽象枪支类进行处理。

如果子类不能完整的实现父类的方法或者父类某些方法在子类中发生了畸变,则应该断开父子关系采用依赖、组合、聚集等关系来代替原有的继承。

玩具枪继承枪支抽象类的情况:射击方法不能被实现,如果实现里面具体逻辑为空则毫无意义,即正常情况下不能实现父类的shoot方法,shoot方法必须去掉,从LSP来看如果去掉,则违背了LSP的第一个原则:子类必须实现父类方法。(代码层面来看如果去掉则会报错)

package des.lsp;

public class ToyGun extends  AbstractGun {
@Override
public void shoot() {
//此方法不能实现,玩具枪不能射击
}
}

解决方法:单独建立一个抽象类玩具类,把与枪支共有的如声音、颜色交给抽象枪支类处理,而玩具枪所特有的玩具类的属性交给抽象玩具类处理,玩具枪类实现玩具抽象类

LSP的定义含义2——子类可以含有自己的特性

如图引入,步枪的实现类即步枪由不同的型号。AUG:狙击枪可以由望远镜功能zoomOut方法。



此处Snipper是狙击手类,狙击手与狙击枪是密不可分,属于组合关系,所以狙击手类直接使用子类AUG。

package des.lsp;
//狙击枪
public class AUG extends Rifle {
//狙击枪特有功能
public void zoomOut(){
System.out.print("通过望远镜观察敌人...");
} @Override
public void shoot() {
System.out.print("AUG射击敌人...");
}
}
package des.lsp;
//狙击手
public class Snipper {
//此处传入参数为子类,组合关系
public void killEnemy(AUG aug){
//观察
aug.zoomOut();
//射击
aug.shoot();
}
}
package des.lsp;

public class Client {
public static void main(String[] args) { Snipper s = new Snipper();
s.killEnemy(new AUG());
}
}

LSP原则:父类不一定能替换子类

package des.lsp;

public class Client {
public static void main(String[] args) {
// write your code here
// Soldier s = new Soldier();
// s.setGun(new Rifle());
// s.killEnemy(); Snipper s = new Snipper();
s.killEnemy((AUG) new Rifle());//此处用父类代替了子类
}
}

报错代码

LSP的定义含义3——覆盖或实现父类方法时输入参数可以被放大

假设有如下场景

父类:方法入参<子类方法入参



场景类调用:父类调用自己方法。

package des.lsp;

import java.util.HashMap;

public class Client {
public static void invoker(){
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map); }
public static void main(String[] args) { invoker();
}
}

输出结果



使用里氏替换原则:把所有父类出现的地方替换为子类

package des.lsp;

import java.util.HashMap;

public class Client {
public static void invoker(){
Son f = new Son();
HashMap map = new HashMap();
f.doSomething(map); }
public static void main(String[] args) {
emy((AUG) new Rifle()); invoker();
}
}

输出结果



我们的本意是调用子类重载的方法,入参为Map的方法,但实际程序执行是调用的从父类继承的方法。如果子类的方法中入参的范围大于父类入参的范围,则子类代替父类的时候,子类的方法永远不会执行。

从另外角度来看,假如父类入参的范围大于子类的入参的范围,则父类替换子类就未必能存在,这时候很可能会调用子类的方法执行。此句话较为抽象,实际情况如下。



父类和子类的代码如下

public class Father {
public Collection doSomething(Map map){
System.out.print("父类被执行...");
return map.values();
}
}
public class Son extends Father {
public Collection doSomething(HashMap map) {
System.out.print("子类执行...");
return map.values();
}
}

场景类:调用父类

package des.lsp;

import java.util.HashMap;

public class Client {
public static void invoker(){
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map); }
public static void main(String[] args) { invoker();
}
}

运行结果:不言而喻,是父类被执行



采用LSP后

package des.lsp;

import java.util.HashMap;

public class Client {
public static void invoker(){
Son f = new Son();
HashMap map = new HashMap();
f.doSomething(map); }
public static void main(String[] args) { invoker();
}
}



此时一般人会想,难道不是子类执行吗?因为子类的入参就是HashMap,肯定要调用这个。

但是此时要考虑一个问题,假如我们的本来意思是就是调用从父类继承的入参为Map的方法,但是程序执行的时候却自动为我们执行了子类的方法,此时就会导致混乱。

结论:子类中的方法的输入参数(前置条件或称形式参数)必须与父类中的输入参数一致或者更宽松(范围更大)。

LSP的定义含义4——覆盖或实现父类的方法时输出结果可以被缩小

理解:父类的返回类型为T,子类的返回类型为S,即LSP要求S<= T

此时分为两种情况

  • 如果时覆写,子类继承父类,继承的方法的入参必然相同,此时传入参数必须时相同或小于,返回的值必然不能大于父类返回值,这是覆写的要求。
  • 如果时重载,这时候要求子类重载方法的参数类型或数量不相同,其实就是保证输入参数宽于或等于父类输入参数,这时候就保证了子类的方法永远不会被执行,其实就是含义3。

LSP的目的及理解

  • 增强程序的健壮性
  • 保证即使增加子类,原有的子类仍然可以继续运行。
  • 从一方面来说,在程序中尽量避免直接使用子类的个性,而是通过父类一步一步的使用子类,否则直接使用子类其实就相当于直接把子类当作父类,这就直接导致父类毫无用途,父类和子类的关系也会显得没有必要存在了。

六大设计原则(二)LSP里氏替换原则的更多相关文章

  1. 设计模式六大原则(二):里氏替换原则(Liskov Substitution Principle)

    里氏替换原则(LSP)由来: 最早是在 妖久八八 年, 由麻神理工学院得一个女士所提出来的. 定义: 1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 ...

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

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

  3. 面向对象设计原则三:里氏替换原则(LSP)

    里氏替换原则(LSP)定义:在任何父类出现的地方都可以用它的子类类替换,且不影响功能.解释说明:其实LSP是对开闭原则的一个扩展,在OO思想中,我们知道对象是由一系列的状态和行为组成的,里氏替换原则说 ...

  4. 【设计模式六大原则2】里氏替换原则(Liskov Substitution Principle)

      肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对 ...

  5. 设计模式原则(2)--Liskov Substitution Principle(LSP)--里氏替换原则

    1.定义: 所有引用基类(父类)的地方必须能透明地使用其子类的对象.这一原则与继承紧密相关.如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的 ...

  6. 面向对象五大原则_1.单一职责原则&amp;2.里氏替换原则

    单一职责原则:Single Responsibility Principle (SRP) 一个类.仅仅有一个引起它变化的原因.应该仅仅有一个职责.每个职责都是变化的一个轴线.假设一个类有一个以上的职责 ...

  7. 面象对象设计原则之三:里氏替换原则(The Liskov Substitution Principle,LSP)

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

  8. 七、LSP 里氏替换原则

    子类的对象提供了父类的所有行为,且加上子类额外的一些东西(可以是功能,可以是属性).当程序基于父类实现时,如果将子类替换父类而程序不需修改,则说明符合LSP原则. 这个解释看的似懂非懂,再看下面更进一 ...

  9. 《设计模式》-原则二:里氏代换原则(LSP)

    回顾一下上一节说的“开闭原则” 说的是 一个软件要遵循对修改关闭 对新功能扩展的原则. 这一次来说说 “里氏代换原则” 意思是说:子类型必须能代替他们的基类. 看了半天的例子 好像 是懂非懂啊...相 ...

随机推荐

  1. Scrapy爬取豆瓣电影top250的电影数据、海报,MySQL存储

    从GitHub得到完整项目(https://github.com/daleyzou/douban.git) 1.成果展示 数据库 本地海报图片 2.环境 (1)已安装Scrapy的Pycharm (2 ...

  2. 团队项目第二阶段个人进展——Day10

    一.昨天工作总结 冲刺第十天,做程序的测试,并修复一些小的bug 二.遇到的问题 无 三.今日工作规划 继续对程序进行测试优化

  3. 条件随机场CRF(二) 前向后向算法评估标记序列概率

    条件随机场CRF(一)从随机场到线性链条件随机场 条件随机场CRF(二) 前向后向算法评估标记序列概率 条件随机场CRF(三) 模型学习与维特比算法解码 在条件随机场CRF(一)中我们总结了CRF的模 ...

  4. 10个Python练手小程序,学习python的很好的资料

    [程序1] 题目:有1.2.3.4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? 1.程序分析:可填在百位.十位.个位的数字都是1.2.3.4.组成所有的排列后再去  掉不满足条件的排列 ...

  5. PAT1094:The Largest Generation

    1094. The Largest Generation (25) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yu ...

  6. 二十、Hadoop学记笔记————Hive On Hbase

    Hive架构图: 一般用户接口采用命令行操作, hive与hbase整合之后架构图: 使用场景 场景一:通过insert语句,将文件或者table中的内容加入到hive中,由于hive和hbase已经 ...

  7. 部署:持续集成(CI)与持续交付(CD)——《微服务设计》读书笔记

        系列文章目录:     <微服务设计>读书笔记大纲 一.CI(Continuous Integration)简介  CI规则1:尽量频繁地把代码签入到分支中以进行集成 CI规则2: ...

  8. 如何在Android Studio中指定NDK位置?

    如何在Android Studio中指定NDK位置? 问题描述 NDK已经手工下载解包在本地: D:\Portable\android-ndk-r13b 每次创建支持C++项目时,都提示NDK没配置, ...

  9. 【新手向】自用的tooltip小插件,前端插件知识科普~

    上面的tooltip就是成品图,为了和自己站点的风格保持一致所以自己写的. 第一部分:你绝对碰到过的匿名函数闭包问题 第二部分:写个tooltip demo 第三部分:源码地址 第一部分 你绝对碰到过 ...

  10. 13.app后端为什么要用到消息队列

    很多没有实际项目经验的小伙伴,对消息队列系统非常陌生,看着很多架构的介绍中,都提到消息队列.但是,不知道为什么要用消息队列?什么是消息队列?常见的消息队列产品有哪些? 通过阅读本文,帮你解开以上的疑惑 ...