设计模式七大原则——里氏替换原则
一、面向对象中继承性的思考和说明
(1)继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
(2)继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低, 增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
(3)问题提出:在编程中,如何正确的使用继承?
答案就是遵循里氏替换原则
二、基本介绍
(1)背景介绍
在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。它由麻省理工学院电子电气与计算机科学系教授芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。里氏替换原则的内容可以描述为:“派生类(子类)对象可以在程式中代替其基类(超类)对象。” 以上内容并非利斯科夫的原文,而是译自罗伯特·马丁(Robert Martin)对原文的解读。芭芭拉·利斯科夫与周以真(Jeannette Wing)在1994年发表论文并提出以上的Liskov代换原则。
(2)具体表述
如果对每个类型为 T1 的对象 O1,都有类型为 T2 的对象 O2,使得以 T1 定义的所有程序 P 在所有的对象 O1 都代换成 O2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题
三、应用实例
先来看一段程序代码:
1 package edu.hbeu.principle.liskov; 2 3 public class Liskov1 { 4 public static void main(String[] args) { 5 A a = new A(); 6 System.out.println("11-3=" + a.func1(11, 3)); 7 System.out.println("1-8=" + a.func1(1, 8)); 8 9 System.out.println("======================="); 10 B b = new B(); 11 System.out.println("11-3=" + b.func1(11, 3));//本意是要求出11-3的结果 12 System.out.println("1-8=" + b.func1(1, 8));//本意是要求出1-8的结果 13 System.out.println("11+3+9=" + b.func2(11, 3)); 14 } 15 } 16 17 18 class A { 19 //返回两数之差 20 public int func1(int num1, int num2) { 21 return num1 - num2; 22 } 23 } 24 25 //增加了一个新功能:完成两个数相加,然后和9求和 26 class B extends A { 27 //重写了A类的方法,这种重写可能是无意识的 28 @Override 29 public int func1(int num1, int num2) { 30 return num1 + num2; 31 } 32 33 public int func2(int a, int b) { 34 return func1(a, b) + 9; 35 } 36 }
运行结果:
分析:
由于类B继承类A之后重写了类A的方法,导致其功能发生变化,最终导致错误的结果,这种重写可能是无意识的,但是在项目开发中可能经常会碰到,所以我们需要遵循里氏替换原则来对继承进行解耦
解决方案:
(1)我们发现原来运行正常的相减功能发生了错误。原因就是类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候
(2)通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合等关系代替。
代码实现:
这里我们采用组合关系来代替继承,类图如下:
具体代码:
1 public class Liskov { 2 public static void main(String[] args) { 3 A a = new A(); 4 System.out.println("11-3=" + a.func1(11, 3)); 5 System.out.println("1-8=" + a.func1(1, 8)); 6 7 System.out.println("======================="); 8 B b = new B(); 9 //B类不再继承A类,因此调用者,不再认为func1是求减法,调用完成的功能就会很明确 10 System.out.println("11+3=" + b.func1(11, 3));//本意是求11+3 11 System.out.println("1+8=" + b.func1(1, 8));//本意是求1+8 12 System.out.println("11+3+9=" + b.func2(11, 3)); 13 14 //使用组合,仍然可以使用到A的相关方法 15 System.out.println("11-3=" + b.func3(11, 3)); 16 } 17 } 18 19 //创建一个更加基础的类 20 class Base { 21 //把更加基础的方法和成员写到Base 22 } 23 24 class A extends Base { 25 //返回两数之差 26 public int func1(int num1, int num2) { 27 return num1 - num2; 28 } 29 } 30 31 class B extends Base { 32 //B类中使用A类的方法,使用组合的关系 33 private A a = new A(); 34 35 public int func1(int a, int b) { 36 return a + b; 37 } 38 39 public int func2(int a, int b) { 40 return func1(a, b) + 9; 41 } 42 43 //仍然使用A类中的方法 44 public int func3(int a, int b) { 45 return this.a.func1(a, b); 46 } 47 }
运行结果:
总结:使原有的父类和子类都继承一个更通俗的基类,并用组合关系来代替原有的继承关系来遵循里氏替换原则,降低了代码的耦合度,解决了使用继承特性时对代码造成的侵入