里氏替换原则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. Github发现优秀的开源项目

    先上个大logo,哈哈. github上有非常多的资源,我们可以在github上搜索到非常多的开源项目.那么如何使用github查找资源? 罗列出一下几种方式. 1.Explore 登录GitHub, ...

  2. git基础命令学习总结

    git版本升级 git clone git://git.kernel.org/pub/scm/git/git.git 列出所有 Git 当时能找到的配置 git config --list git c ...

  3. 最近面了不少java开发,据此来说下我的感受:哪怕事先只准备1小时,成功概率也能大大提升

    本人最近几年一直在做java后端方面的技术面试官,而在最近两周,又密集了面试了一些java初级和高级开发的候选人,在面试过程中,我自认为比较慎重,遇到问题回答不好的候选人,我总会再三从不同方面提问,只 ...

  4. 使用Iterator迭代器循环集合

    1.Iterator迭代器用于遍历集合元素,获取迭代器可以使用. 2.Iterator提供了统一遍历集合元素的 方式 ,其提供了用于遍历集合的连个方法----- boolean  hasNext()判 ...

  5. 【Java入门提高篇】Java集合类详解(一)

    今天来看看Java里的一个大家伙,那就是集合. 集合嘛,就跟它的名字那样,是一群人多势众的家伙,如果你学过高数,没错,就跟里面说的集合是一个概念,就是一堆对象的集合体.集合就是用来存放和管理其他类对象 ...

  6. 错误 C2280 Union : 尝试引用已删除的函数 以及 警告 C4624 “Grade”: 已将析构函数隐式定义为“已删除”的一种解决方法

    Union 是C/C++语言中的一种结构类型,用于定义可共享内存的数据变量的一种方式,初次使用Union联合体时可能会遇到以下问题: 错误 C2280 Union : 尝试引用已删除的函数 警告 C4 ...

  7. Get Docker CE for CentOS

    To get started with Docker CE on CentOS, make sure you meet the prerequisites, then install Docker. ...

  8. SVN使用教程2017.10.6

    http://www.cnblogs.com/mq0036/p/5250198.html

  9. 对于bootstrap可视化布局设计可以参考2017.6.2

    http://www.layoutit.cn/v3/index.html  可视化布局链接,拖好控件自动生成代码非常方便 http://www.jq22.com/jquery-info14044  这 ...

  10. tkinter中menu菜单控件(十二)

    menu菜单控件 import tkinter wuya = tkinter.Tk() wuya.title("wuya") wuya.geometry("300x200 ...