里式替换原则(ISP)

里氏替换原则定义

里氏替换原则(Liskov Substitution Principle,LSP):
第一种定义:

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换为o2,程序P的行为没有发生变化,那么类型S是类型T的子类型。

第二种定义:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

所有引用基类的地方必须透明的使用其子类的对象。

第二种定义明确的说,只要父类能出现的地方子类也可以出现,而且替换为子类不会产生任何错误或异常,但是反过来就不行,有子类出现的地方,父类未必就能适应。

爱恨纠葛的父子关系

在面向对象的语言中,继承是必不可少的,非常优秀的语言机制,它有如下优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
  • 提高代码的重用性
  • 子类可以形似父类,但是又异于父类。
  • 提高代码的可扩展性,实现父类的方法就可以了。许多开源框架的扩展接口都是通过继承父类来完成。
  • 提高产品或项目的开放性

但是所有的事物都有二面性,继承除了有上述优点,也有下面缺点:

  • 继承是侵入性的,只要继承,就必须拥有父类的所有方法和属性
  • 降低了代码的灵活性,子类必须拥有父类的属性和方法,让子类有了一些约束
  • 增加了耦合性,当父类的常量,变量和方法被修改了,需要考虑子类的修改,这种修改可能带来非常糟糕的结果,要重构大量的代码。

java使用extends关键字来实现继承,它采用单一继承规则,C ++则采用多重继承。从整体来看,利大于弊。怎么才能让利的因素发挥最大的作用,同时减少弊的影响?解决方案是引入里氏替换原则。

里氏替换原则的规范

里氏替换原则为良好的继承定义了一个规范,主要有下面四个方面:

子类必须完全实现父类的方法

我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这已经使用了里氏替换原则。

我们举个射击的例子,其类图如下:

枪支类图

枪支抽象类:

public abstract class AbstractGun{
   //枪支射击的方法
   public abstract void shoot();
}

手枪,步枪,机枪的实现类:

public class Handgun extends AbstractGun{

   public void shoot(){
      System.out.println("手枪射击...");
   }
}
public class Rifle extends AbstractGun{

   public void shoot(){
      System.out.println("步枪射击...");
   }
}
public class CachineGun extends AbstractGun{

   public void shoot(){
      System.out.println("机枪射击...");
   }
}

士兵实现类:

public class Soldier{

  private AbstractGun gun;

  public void setGun(AbstractGun gun){
    this.gun = gun;
  }

  public void killEnemy(){
    System.out.println("士兵开始射击...");
    this.gun.shoot();
  }
}

场景类:

public class Client{
   public static void main(Strings[] args){
     Soldier soldier = new Soldier();
     //设置士兵手握手枪
     soldier.setGun(new Handgun());
     soldier.killEnemy();
     //设置士兵手握步枪
     soldier.setGun(new Rifle());
     soldier.killEnemy();
     //设置士兵手握机枪
     soldier.setGun(new CachineGun());
     soldier.killEnemy();

   }

}

注意:

如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸变,则建议断开继承关系,采用依赖,聚集,组合等关系代替继承。

子类可以有自己的个性

子类当然可以有自己的行为和外观,也就是方法和属性。但是里氏替换原则可以正着用,但是不能反着用。在子类出现的地方,父类未必就可以胜任。还是以刚才枪支为例,在步枪中,有一些枪支比较有名,比如AK47,AUG狙击步枪等,我们把这二个型号的枪支引入后的类图如下:

增加AK47和AUG后的Rifle子类图

AUG狙击步枪:

public class AUG extends Rifle {

   public void zoomOut(){
      System.out.println("通过放大镜观察");
   }

   public void shoot(){
      System.out.println("AUG射击...");
   }

}

狙击手类:

public class Snipper{

  public void killEnemy(AUG aug){
     aug.zoomOut();
     aug.shoot();
  }

}

场景类:

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

运行结果:

通过放大镜观察
AUG射击...

运行结果:

通过放大镜观察
AUG射击...

在这里,系统直接调用了子类,狙击手类是依赖枪支的,所以我拉直接把子类AUG传递进来,这个时候,我们可以把父类传递进来吗?修改一下Client类:

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

调用程序,发现不行,会抛出异常(java.lang.ClassCastException)。这也就是大家经常说的向下转换是不安全的,从里氏替换原则来看,就是有子类出现的地方父类不一定能出现。

覆盖或实现父类的方法时输入参数可以被放大

方法中的输入参数称为前置条件,我们以一个例子说明一下覆盖或实现父类的方法时输入参数可以被放大:

我们先定义一个Father类:

import java.util.Collection;
import java.util.HashMap;

public class Father{
  public Collection doSomething(HashMap map){
     System.out.println("父类被执行");
     return map.values();
  }
}

再定义一个子类:

import java.util.Collection;
import java.util.Map;

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

场景类:

public class Client{
   public static void main(Strings[] args){
       //父类存在的地方,子类应该可以存在
       Father father = new Father();
       HashMap map = new HashMap();
       father.doSomething(map);
   }
}

运行结果:

父类被执行

根据里氏替换原则,父类出现的地方,子类也是可以出现的。我们把Client代码修改如下:

public class Client{
   public static void main(Strings[] args){
       //父类存在的地方,子类应该可以存在
       //Father father = new Father();
       Son father = new Son();
       HashMap map = new HashMap();
       father.doSomething(map);
   }
}

运行结果:

父类被执行

结果一样,父类的方法的输入参数是HashMap类型,子类的方法输入参数是Map类型,也就是说子类的输入参数类型范围扩大了,子类代替父类,子类的方法不被执行,这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。

如果,我们反过来,把父类的输入参数类型放大,子类的输入参数类型缩小,让子类的输入参数类型小于父类的输入参数类型,看看会出现什么情况?

父类前置条件较大:

import java.util.Collection;
import java.util.Map;

public class Father{
  public Collection doSomething(Map map){
     System.out.println("父类被执行");
     return map.values();
  }
}

子类的前置条件较小:

import java.util.Collection;
import java.util.HashMap;

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

场景类:

public class Client{
   public static void main(Strings[] args){
       //父类存在的地方,子类应该可以存在
       Father father = new Father();
       HashMap map = new HashMap();
       father.doSomething(map);
   }
}

运行结果:

父类被执行

我们再把里氏替换原则引入,父类出现的地方子类也可以出现,我们修改一下Client类,看看有什么问题:

public class Client{
   public static void main(Strings[] args){
       //父类存在的地方,子类应该可以存在
       //Father father = new Father();
       Son father = new Son();
       HashMap map = new HashMap();
       father.doSomething(map);
   }
}

输出结果:

子类被执行

看到了吧,调用了子类,子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务逻辑混乱,因为在实际应用中父类一般是抽象类,子类是实现类,你传递一个这样的实现类就会歪曲了父类的意图,引起业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或更宽松。

覆盖或实现父类的方法时输出结果可以被缩小

父类的一个方法的返回值是一个类型T,子类的相同方法的返回值为S,那么里氏替换原则就要求S必须小于等于T。

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美。

里氏替换原则经验

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有了“个性”,这个子类和父类之间的关系就难调和,把子类当做父类使用,子类的“个性”被抺杀了,把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离–缺乏类替换的标准。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 3,743评论 0 14
  • 单一职责原则 (SRP) 全称 SRP , Single Responsibility Principle 单一职...
    米莉_L阅读 1,765评论 2 5
  • 文/孤鸟差鱼 你的故事 总是少了梗概 读起来凌乱生硬 那些年 我们为一句话落下的争执 多年再也遇不到后 才都觉得是...
    孤鸟差鱼阅读 170评论 2 8
  • 何止七十二变化,还赠三根救命毫, 平行宇宙如来去,万花筒中如梦好。
    雪中凝阅读 189评论 0 5
  • 0起因 test服务器经常要手动在本地执行部署脚本,很是麻烦,索性决定配置CI,当test分支有push动作的时候...
    __XY__阅读 335评论 0 0