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

来源:互联网 发布:php域名跳转代码 编辑:程序博客网 时间:2024/04/28 10:00

里氏替换原则

里氏替换原则: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");    }}

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