java设计原则--里氏替换原则

JAVA设计原则–里氏替换原则(LSP原则)


为什么要用里氏替换原则?:

为了优化继承所带来的缺点,使得继承的优点发挥到最大,而同时减少缺点带来的麻烦。

继承的优缺点:

优点:
1. 代码共享,减少创建类的工作量,每个子类都拥有父类的属性和方法
2. 提高代码的重用性(子类可以使用父类的属性和方法)
3. 子类可以在父类的基础上进行拓展(重写父类的方法,实现自己的逻辑)(很多开源框架的扩展接口都是通过继承父类来实现的)。

缺点:
1. 继承是具有侵入性的。也就是只要继承,就必须拥有父类的所有属性和方法
2. 降低了子类的灵活性。同强增加了耦合性。(因为子类具有父类的属性和变量,所以,在修改父类的属性和方法时,需要考虑子类的修改,而这种修改如果没有规范,可能需要大段的代码重构)

里氏替换原则的规范(定义):

1、所有引用父类的地方都必须能透明地使用其子类的对象。
(只要哪里使用了父类,那么他的所有子类也必须能使用,替换子类不会发生任何错误或异常,调用者不需要知道是子类还是父类。)

2、子类出现的地方,父类不一定能适用。

里氏替换原则规范的含义:

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

例: 在平常编写代码时,定义接口,然后编写实现类,而在调用时,直接调用接口方法(高层抽象)(其他场景下还有抽象类这种情况),而不去调用具体的实现了,其实这里已经使用了里氏替换原则。

例:士兵射击的场景—–枪(本篇中的例子均来自<<设计模式之禅>>)

枪有很多种类,具体士兵使用什么枪,得等到调用的时候才知道,所以需要我们对枪进行抽象:

枪支的抽象类:
public abstract class AbstractGun {
     //定义模板射击方法 具体让子类去实现
     public abstract void shoot();

     // 枪的形状
     public void shape() { // 枪的形状};

     // 枪的声音
     public void voice( // 枪的声音);
}
// 手枪
public class Handgun extends AbstractGun {
      // 手枪的射击方法
      @Override
      public void shoot() {
          System.out.println("手枪射击");
      }
}

// 步枪
public class Rifle extends AbstractGun {
     // 步枪的射击
     @Override
     public void shoot() {
         System.out.println("步枪射击");
     }
}

// 机枪
public class MachineGun extends AbstractGun {
     // 机枪
     @Override
     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();
     }
}

注意: 在Soldier类中,调用枪类时,调用了顶级父类AbstractGun ,这符合了里氏替换原则(LSP):
在类中调用其他类时,务必使用父类或接口(高层抽象),如果不能使用父类或接口,则说明类的设计已经违背了原则。

士兵有了,枪支有了,之后就需要在实际中(具体场景)去用具体的枪射击敌人:

public class client {
     public static void main(String[] args) {
          // 产生士兵
          Soldier soldier = new Soldier();
          // 给士兵枪支 这里给了步枪 如果要使用其他的枪,传入其他具体的子类即可
          soldier.setGun(new Rifile);
          // 士兵射击敌人
          soldier.killEnemy();
     }
}

注意:当出现特殊的子类,并且无法应用在父类的场景下时,应当对子类进行抽象,建立一个独立的父类,并将枪支的抽象类与特殊的子类的抽象类建立委托关系。

// 因为玩具枪不满足场景(不能杀敌人),所以得独立建立父类,然后两个父类下的子类各自延展
public abstract class AbstractToy {
     // 与枪支的抽象类建立委托关系 将声音、形状等等一些特性都委托给AbstractGun处理
     private AbstractGun gun;

     // 委托给AbstractGun处理 
     public void shape() {
         System.out.println("枪的形状");
         gun.shape();
     };

     // 委托给AbstractGun处理 
     public void voice() {
         System.out.println("枪射击的声音");
         gun.voice();
     }
}

public class ToyGun extends AbstractToy {

     // 玩具枪形状
     super.shape();

     // 玩具枪声音
     super.shape();
}

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

2.子类可以存在属于自己的属性和方法

里氏替换原则可以正着用,但是不能反着用(在子类出现的地方,父类未必能使用。)

public class AUG extends Rifile {
    // 狙击枪带望远镜
    public void zoomOut() {
        System.out.println("通过望远镜观察敌人");
    }

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

// AUG狙击手
public class Snipper {
    public void killEnemy(Aug aug) {
        // 观察敌人
        aug.zoomOut();
        // 开始射击
        anu.shoot();
    }
}

// 客户端调用(使用子类的场景)
public class Client {
    public static void main(String[] args) {
        Snipper snipper = new Snipper();
        snipper.setRile(new AUG());
        snipper.killEnemy();
    }
}

// 客户端调用(使用父类替代子类)
public class Client {
    public static void main(String[] args) {
        Snipper snipper = new Snipper();
        snipper.setRile((AUG) new Rifle());
        snipper.killEnemy();
    }
}

编写上面那段代码,会发现在运行期间会抛出java.lang.ClassCastException异常,也就是说,向下转型是不安全的,从里氏替换这个原则上来看,就是子类出现的地方,父类不一定能使用。

3.重写或者实现父类的方法时,输入参数可以被放大

例:

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

// 子类返回Collection集合类型
public class Son extends Father {
    // 放大输入参数类型
    public Collection doSomething(Map map) {
        System.out.println("子类被执行");
        return map.values();
    }
}

// 场景类
public class Client {
    public static void invoker() {
        // 父类存在的地方,子类可以替代
        Father father = new Father();
        HashMap map = new HashMap();
        father.doSomething(map);
    }

    public static void main(String[] args) {
        invoker();
    }
}

// 场景类
public class Client {
    public static void invoker() {
        Son son = new Son();
        HashMap map = new HashMap();
        son.doSomething(map);
    }
    public static void main(String[] args) {
        invoker();
    }
}

// 然后会发现,上面两种场景下执行结果相同。

注意: 在上面例子中,子类和父类的方法名相同,但是参数列表不同(方法参数类型不同),所以,这不是重写,而是重载,因为继承是让子类拥有父类的属性和方法,所以在子类中,有两个方法,方法名相同,但是参数列表不同,所以是重载。

父类的参数类型是HashMap,子类的参数类型是Map,说明参数类型的范围被扩大了,当传入的参数是HashMap时,会发现子类代替父类执行(因为继承了父类的方法),而真正的子类方法不会被调用。而子类想执行方法,必须重写或重载父类的方法,这样做是正确的。

因为如果父类参数的类型范围大于子类的话,那么父类出现的地方,子类未必可以使用,可能导致程序出错。

总结: 子类的参数的范围类型必须大于等于父类的参数的范围类型

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

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

第一种情况: 重写:

父类和子类的同名方法的输入参数是相同的,放个方法的范围值S小于等于T,这个是重写的要求(这是为了向上转型;既然子类重写了父类的方法,有时候就需要用父类对象引用来调用子类重写的方法)

第二种情况:重载

要求方法的输入参数类型或数量不相同,根据里氏替换原则,就是子类的参数范围要大于或等于父类的参数范围,也就是说,你写的方法是不会被调用的。

猜你喜欢

转载自blog.csdn.net/zxzzxzzxz123/article/details/79656677