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

一曲冷凌霜 提交于 2019-11-27 04:58:31

1、里氏替换原则来源

继承优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但又易于父类;
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了;
  • 提高产品或者项目的开放性;

继承缺点:

  • 继承是侵入性的,只要是继承,就必须拥有父类的所有属性和方法;
  • 降低代码灵活性,子类必须拥有父类的属性和方法,子类在自由世界中多了些约束;
  • 增强了耦合性,当父类的常量,变量或者方法被修改时,需要考虑子类的修改

Java用extends关键字来实现继承,它采用了单一继承的规则,而C++则采用了多重继承的规则,一个子类可以继承多个父类。从总体上看,单继承利大于弊。如何把“利”发挥最大作用,同时减少“弊”带来的问题。解决方案就是引入里氏替换原则(LSP)。

  • 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
  • 所有引用基类的地方必须能透明地使用其子类的对象。

对上面第二种解释为“只要父类能出现的地方,子类都可以出现,而且替换为子类也不会产生任何错误或者异常,使用者不需要知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就能适应。”

2、里氏替换原则规则

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

我们先定义一个枪的接口

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 MachineGun 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("士兵开始杀敌人...");
        gun.shoot();
    }
}

场景类

public class Client {
  public static void main(String[] args) {
    //产生三毛这个士兵
    Soldier sanMao = new Soldier();
    //给三毛一支枪
    sanMao.setGun(new Handgun());
    sanMao.killEnemy();
  }
}

运行结果:

士兵开始杀敌人...
手枪射击...

在这个过程,我们给三毛这个士兵一把手枪,然后他就开始杀敌了。如果三毛要用机枪杀敌,sanMao.setGun(new Handgun());换成sanMao.setGun(new MachineGun());所以在编写士兵类的时候根本就不用知道是哪个型号的枪被传入。

注意: 在类中调用其他类时务必要使用父类接口,如果不能使用父类或接口,则该类已经违背了LSP原则

b)子类可以有自己的个性

这个不想过多解释,主要是注意向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

c)覆盖或实现父类的方法时输入参数可以被放大
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父类被执行...");
return map.values();
}
}
public class Son extends Father {
//放大输入参数类型
public Collection doSomething(Map map){
System.out.println("子类被执行...");
return map.values();
}
}

如上面的例子,子类传入的参数是Map,而父类传入的是HashMap,不错,是重载,子类的参数被放大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。如果想让子类的方法运行,就必须覆写父类的方法。

如果子类的参数范围比父类小,会引起只调用子类方法,引起程序混乱。

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

父类的一个方法的返回值是一个类型T,子类的相同方法(重载或重写)的返回值是S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类

  • 重写:父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是重写的要求(重点);
  • 重载:方法的输入参数类型后者数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,就是你写的这个方法不会被调用。

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

注意:如果采用里氏替换原则,那么尽量避免子类的“个性”,一个子类有个性,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的个性“被抹杀”;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离,缺乏类替换的标准。

参考:《设计模式之禅》

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!