查看原文
其他

六大设计原则详解(2)-里氏替换原则

2017-09-15 qzs 安卓干货铺

简介:

里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。

在面向对象的语言中,我对继承应该很熟悉了,里氏替换原则是给继承定义一个规则。
里氏替换原则包含了四层含义。

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

如果子类不能完整的实现父类的方法,或者在实现中加入了一些特定的”条件”,则应该断开继承关系。

实例:
首先写一个动物的父类Animal:

public  abstract class Animal {    //动物生活在哪?    public abstract void   WalkingMode(); }

然后下面是三个子类

public class Dog extends Animal{    @Override    public void Classification() {    System.out.print("狗是陆地动物");    } }

public class Cat  extends Animal{    @Override    public void Classification() {    System.out.print("猫是陆地动物");    } }

public class Fish  extends Animal{    @Override    public void Classification() {        System.out.print("鱼是海洋动物");    } }

现在在一个Demo类中调用,任务是执行show方法并符合里面的内容,下面以Dog为例:

public class Demo {    public Animal animal;    public static void main(String[] args) {        Demo demo=new  Demo();        demo.set_Animal(new Dog());        demo.show();    }    public void set_Animal(Animal animal){        this.animal=animal;        }    public void show(){        System.out.print("这些动物都是陆地动物   ");  //注意:调用其他类时必须要用父类!        animal.Classification();        } }

输出的结果肯定是”这些动物都是陆地动物 狗是陆地动物”,但是如果要是以Fish为例的话,输出的结果是”这些动物都是陆地动物 鱼是海洋动物”不符合题意。有时我们遇到这种情况会加个if else判断一下,这样的代码可行,但是对于在项目中有很多这样的类,不仅逻辑繁琐而且还容易出现重大错误。这时最好的解决方法就是把fish类与Animal类脱离继承关系,用其他的关系代替或者说在设计时就把父类的方法写好,分类全面。

子类可以增加自己特有的方法:

这句话相信大家应该都很清楚了。
需要注意的一点: 当子类有自己的特有的方法时,我们在Demo类中调用一下子类的方法,用子类对象调是没问题的,但是用父类的对象调子类的方法不定成功,有子类出现的地方父类未必就可以出现,会出现异常,因为也许调用的是子类特有的方法,向下转型是不安全的。

再说下面两层含义之前先要明白 重载 重写(覆盖) 的区别:

重写(覆盖)的规则:
1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写而是重载.
2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)。
3、重写的方法的返回值必须和被重写的方法的返回一致;
4、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类;
5、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。
6、静态方法不能被重写为非静态的方法(会编译出错)。

重载的规则:
1、在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样);
2、不能通过访问权限、返回类型、抛出的异常进行重载;
3、方法的异常类型和数目不会对重载造成影响;

类的方法重载父类的方法时,方法的前置条件(形参)要比父类方法的输入参数更宽松

实例:
首先A为父类,B为子类

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

public class B  extends A{    public Collection dosomething(Map  map){        System.out.println("子类被执行了");        return map.values();    } }

在Demo_2中调用:

public class Demo_2 {    public static void main(String[] args) {        Show();    }    public  static void Show(){          A a=new  A();//父类对象           HashMap  map=new HashMap<>();           a.dosomething(map);    } }

打印的结果为”父类被执行了”,因为本来就用的是父类的对象,根据里氏替换原则,父类出现的地方子类就可以出现,我们把Demo_2中的父类对象修改为子类对象。

public class Demo_2 {    public static void main(String[] args) {        Show();    }    public  static void Show(){           B b=new  B();//子类对象           HashMap  map=new HashMap<>();           b.dosomething(map);    } }

运行后发现打印的结果还是”父类被执行了”,这就证实了我们的观点,父类方法的参数是HashMap类型,而子类的方法参数是Map类型,子类的参数类型范围比父类大,那么子类的方法永远也不会执行,这是正确的。
如果我们反过来让父类的参数类型范围大于子类,并在调用时用子类去调用,我们会发现打印时的结果竟然是”子类被执行了”,这在开发中很容易引起业务逻辑的混乱,所以类的方法重载父类的方法时,方法的前置条件(形参)要比父类方法的输入参数更宽松(相同也可以)。

覆写或者实现父类的方法时输出结果(返回值)可以被缩小

这句话应该很好理解,比如父类A的方法的返回值类型是X,子类实现父类的方法,它的返回值类型是Y。根据里氏替换原则,X和Y可以相同,或者Y是X的子类!

总结

理解里氏替换原则包含的四层含义,在开发中,尽量避免子类的“个性”。
有子类出现的地方父类未必就可以出现
父类出现的地方子类就可以出现

‘最近在学习Android设计模式,参考的书籍有《设计模式之禅》《Android进阶之光》等等。

我的交流圈如下-期待大家的加入:



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存