【设计模式之禅】第2章 里氏替换原则

引入里氏替换原则(LiskovSubstitution Principle,LSP)的目的就是为了解决继承中父子类之间产生的强耦合性。

2.1 爱恨纠葛的父子关系

继承

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制

继承的优点

● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;

● 提高代码的重用性;

● 子类可以形似父类,但又异于父类,"龙生龙,凤生凤,老鼠生来会打洞"是说子拥有父的"种","世界上没有两片完全相同的叶子"是指明子与父的不同;

● 提高代码的可扩展性,实现父类的方法就可以"为所欲为"了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;

● 提高产品或项目的开放性。

继承的缺点

● 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;

● 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;

● 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。

从整体上来看,利大于弊,怎么才能让"利"的因素发挥最大的作用,同时减少"弊"带来的麻烦呢?

解决方案是引入里氏替换原则(LiskovSubstitution Principle,LSP)。

里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)

  1. 第一种定义:

虽然比较正宗,但比较晦涩,了解即可

  1. 第二种定义:

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

解释:【】

只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。

但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

2.2 纠纷不断,规则压制

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。

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

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

  1. // 抽象枪支
  2. public
    abstract
    class AbstractGun {
  3.    public
    abstract
    void shoot();
  4. }
  5. public
    class HandGun extends AbstractGun {
  6.  
  7.    @Override
  8.    public
    void shoot() {
  9.       System.out.println("手枪开枪:啪啪啪");
  10.    }
  11.  
  12. }
  13. public
    class Rifle extends AbstractGun {
  14.  
  15.    @Override
  16.    public
    void shoot() {
  17.       System.out.println("步枪开枪:叭叭叭");
  18.    }
  19.  
  20. }
  21. public
    class MachineGun extends AbstractGun {
  22.  
  23.    @Override
  24.    public
    void shoot() {
  25.       System.out.println("机枪开枪:哒哒哒");
  26.    }
  27.  
  28. }
  29. public
    class Soldier {
  30.    // 有一把枪
  31.    private AbstractGun ag;
  32.  
  33.    public AbstractGun getAg() {
  34.       return ag;
  35.    }
  36.  
  37.    public
    void setAg(AbstractGun ag) {
  38.       this.ag = ag;
  39.    }
  40.  
  41.    // 杀敌
  42.    public
    void killEnemy(){
  43.       System.out.println("士兵上战场杀敌");
  44.       ag.shoot();
  45.    }
  46. }
  47. public
    class Client {
  48.    public
    static
    void main(String[] args) {
  49.       // 创建士兵
  50.       Soldier s = new Soldier();
  51.       // 给一支枪
  52.       //s.setAg(new HandGun());
  53.       //s.setAg(new Rifle());
  54.       s.setAg(new MachineGun());
  55.       // 上阵杀敌
  56.       s.killEnemy();
  57.       System.out.println("--------------");
  58.  
  59.       // 创建狙击手对象
  60.       Snipper sanMao = new Snipper();
  61.       // 给狙击手狙击步枪
  62.       sanMao.setAug(new AUG());
  63.       // 创建父类向下转型作为参数
  64.       //sanMao.setAug((AUG)(new Rifle())); // java.lang.ClassCastException:根本就没有多态何来向下转型之说呢
  65.       // 杀死敌人
  66.       sanMao.killEnemy();
  67.    }
  68. }

在编写程序时Solider士兵类根本就不用知道是哪个型号的枪(子类)被传入。

注意事项1

在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。

继承就是告诉你拥有父类的方法和属性,然后你就可以重写父类的方法。

但是在具体应用场景中就要考虑下面这个问题了:子类是否能够完整地实现父类的业务。

注意事项2

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

2.子类可以有自己的个性

子类当然可以有自己的行为和外观了,也就是方法和属性。

有时候需要直接方法上的参数需要直接传递子类进来,可能不需要多态,不过这种情况比较少。

从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

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

方法中的输入参数称为前置条件,"契约优先"的原则。

子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同(重写)或者更宽松(重载)。

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

父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,

那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类

重写

父类和子类的同名方法的输入参数是相同的,两个方法的范围值子类方法返回值S小于等于父类方法返回值T。

重载

要求方法的输入参数类型或数量不相同,子类的输入参数宽于或等于父类的输入参数

保证父类的地方替换成子类,不会调用子类与父类的同名方法。

总结

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。

在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!

2.3 最佳实践

在项目中,采用里氏替换原则时,尽量避免子类的"个性",一旦子类有"个性",

这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的"个性"被抹杀——委屈了点;

把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。

时间: 2024-10-06 15:25:45

【设计模式之禅】第2章 里氏替换原则的相关文章

设计模式六大原则之里氏替换原则

一.概念: 里氏替换原则:LSP (Liskov Substitution Principle),如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型. 通俗的定义:所有引用基类的地方必须能透明地使用其子类的对象. 二.例子: 以浇水为例.人,拿到工具[水管.水桶.瓶子],装水后都可以浇水.[水管.桶.瓶子]都可以获取水.应该有个loadWater方法.有watering 浇水功能

学习设计模式 - 六大基本原则之里氏替换原则

设计模式总共有六大基本原则,统称为SOLID (稳定)原则,分别是S-单一职责原则(Single Responsibility Principle), O-开闭原则(Open closed Principle),L-里氏替换原则(Liskov Substitution Principle),L-迪米特法则(Law of Demeter),I-接口隔离原则(Interface Segregation Principle),D-依赖倒置原则(Dependence Invension Principl

设计模式六大原则(2):里氏替换原则

里氏替换原则 肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由

设计模式六大原则(2):里氏替换原则(转载)

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

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

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

小菜学设计模式——里氏替换原则

背景 本文标题为什么叫小菜学习设计模式,原因是本文内容主要是学习<大话设计模式>时的笔记摘要部分,当然,并不是记录书中小菜的学习过程,这个完全没有意义,而是指本人学习设计模式的成长之旅. 真诚的希望自己能够从一名小菜成长为一名大鸟! 编写的程序应该满足: 1)可维护 2)可扩展 3)可复用 4)够灵活 废话少说,言归正传,设计模式原则之:里氏替换原则 书面理解 里氏替换原则:一个软件实体如果使用的是父类的话,那么一定适用与其子类,而且它察觉不出父类对象和子类对象的区别.也就是说,在软件里面,把

【设计模式】单一职责 开-闭 依赖倒转 里氏替换原则

几个设计模式的原则,简单了解了一下 单一职责 类的功能应该单一,就一个类而言,应该仅有一个引起它变化的原因,否则就要拆分. [大话设计模式]里大鸟和小菜用的DV的摄像功能和手机的摄像功能的比较,DV的功能单一,手机的功能多而复杂,小菜在看到UFO的时候赶紧拿出手机来录像,结果发现录的很不清楚,如果是DV的话,小菜应该来得及摆一个牛逼的姿势,把UFO给录进去. 开闭原则 可以很简单的总结为一句话:对扩展开放,对修改关闭. 对一个功能的修改应该是通过增加代码的方式,而不是通过修改代码的方式.(内心独

&amp;quot;围观&amp;quot;设计模式(2)--里氏替换原则(LSP,Liskov Substitution Principle)

在面向对象的程序设计中.里氏替换原则(Liskov Substitution principle)是对子类型的特别定义.它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为"数据的抽象与层次"的演说中首先提出. 里氏替换原则的内容能够描写叙述为: "派生类(子类)对象能够替换其基类(超类)对象被使用." 以上内容并不是利斯科夫的原文,而是译自罗伯特·马丁(Robert Martin)对原文的解读. 其原文为: Let be a prope

设计模式的七大原则(4) --里氏替换原则

前言 上一节中我们介绍了,依赖倒置,依赖倒置利用抽象的稳定性来架构我们的系统,是我们经常能遇到的一种原则,比如说面向接口编程. 这一节中,我们来说说里氏替换原则,这个原则其实非常非常的简单,其实与依赖倒置相结合来看,就是希望我们用抽象的方法来构建项目而非具体的实现,里氏替换原则就是推荐我们不要重写父类中具体的实现来构建我们的项目. 我们来深入研究研究. 基本介绍 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对