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

前端之家收集整理的这篇文章主要介绍了设计模式6大原则(2):里氏替换原则前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

里氏替换原则

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

刚看到这项原则的时候很困惑,完全不懂什么意思,不过根据西方人思维,喜欢用人名来命名,以纪念或彰显某个人的功绩等等,猜测是一个叫里氏的人提出来的。后来查阅维基百科,这个原则由麻省理工学院的芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。

这里说说Barbara,很牛x的一位女性,美国第一个获得计算机科学博士学位的女性(1968年,斯坦福大学),1972年成为麻省理工学院的教授。2008年度美国计算机学会(ACM)图灵奖(Turing Award)获得者。为计算机的发展做出了很大贡献。

言归正题,说里氏替换之前,不得不先说说继承。

以下这段摘自《设计模式之禅》
 
在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

    代码共享,减少创建类的工作量,每个子类都拥有父类方法属性;
    提高代码的重用性;
    子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
    提高代码的可扩展性,实现父类方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
    提高产品或项目的开放性。


 
自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:

    继承是侵入性的。只要继承,就必须拥有父类的所有属性方法;
    降低代码的灵活性。子类必须拥有父类属性方法,让子类自由的世界中多了些约束;
    增强了耦合性。当父类的常量、变量和方法修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构。
 
Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?


里氏替换原则就是用来描述使用继承的一种原则,达到利最大,弊最小。


里氏替换的定义有两种,原始的说法是:

If for each object o1 of type S there is an object o2of type T such that for all programs P defined in terms of T,the behavior of Pis unchanged when o1 is substituted for o2 then S is a subtype of T.如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

这个定义有些晦涩,不过仔细想想也好理解。第二种定义比较好理解:

functions that use pointers or references to baseclasses must be able to use objects of derived classes without knowing it.所有引用基类的地方必须能透明地使用其子类的对象。

其实就是高层不应该依赖底层,父类可以替换成子类,即子类必须能够完全代替父类,而父类不能替换子类,否则就是不合理的继承。

具体有4层含义

1.子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

2.子类中可以增加自己特有的方法

3.当子类覆盖或实现父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。


前两个含义意思就是,子类可以扩展父类功能,但是不能修改父类原有的功能,如果修改了,子类就不能替换父类了,这个子类就太有个性了,里氏替换就是不让子类有个性,子类一旦有个性,就只能单独使用子类,这样会让类间的耦合关系变得扑朔迷离——缺乏类替换标准,比如。

class Math {
    public int func(int a,int b) {
       return a + b;
    }
}
class MathExt extends Math {
    @Override
    public int func(int a,int b) {
       return a - b;
    }
}
class Client {
    public static void main(String[] args) {
       Math math = new Math();
       Math math2 = new MathExt();
    }
}

MathExt虽然继承Math了,但是math和math2实现了不同的两个功能,使用者完全不明白这种继承关系的含义。

后两个含义也是前两个含义的补充,第3个含义,子类方法传入的参数要比父类宽松,即子类接收的参数要大于父类,要能包含父类,这样使用类型是父类,实例是子类时,能保证传入的参数一定能符合要求。比如下面这段代码

class Father {
    public void func(HashMap m) {
       System.out.println("执行父类...");
    }
}
class Son extends Father {
    public void func(Map m) {// 方法的形参比父类的更宽松
       System.out.println("执行子类...");
    }
}

最后一个,子类返回值要比父类的严格,即子类返回值范围要小于父类,这样才能保证使用父类时,子类返回值一定符合父类要求。比如下面这段代码

abstract class Father {
    public abstract Map func();
}
 
class Son extends Father {
    @Override
    public HashMap func() {// 方法的返回值比父类的更严格
        return new HashMap();
    }
}

其实,我们会发现在自己编程中常常会违反里氏替换原则,而且有时候故意这么做的,比如子类继承父类了,而且为了不让别人使用某个父类方法,子类覆写时,里面直接抛出异常。比如:

class Father {
    public void fuck(String name) {
        System.out.println("fuck" + name);
    }
}
 
class Son extends Father {
    @Override
    public void fuck(String name) {// 方法不想让别人用
        throw new RuntimeException("We need to be polite");
    }
}

这样也没有问题,还是那句话,原则是死的,人是活的,这么写不好,我们应该避免,但有时候受各方面因素制约,我们也要有取舍。非要不遵循里氏替换原则的后果就是:你写的代码出问题的几率将会大大增加

猜你在找的设计模式相关文章