您的位置:首页 > 其它

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

2017-09-15 09:55 134 查看

简介:

里氏替换原则(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进阶之光》等等。

我的交流圈如下-希望大家多多支持



我的公众号



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: