里氏替换原则
里氏替换原则: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.子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
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"); } }
这样也没有问题,还是那句话,原则是死的,人是活的,这么写不好,我们应该避免,但有时候受各方面因素制约,我们也要有取舍。非要不遵循里氏替换原则的后果就是:你写的代码出问题的几率将会大大增加。