里氏替换原则

里氏替换原则

  1. 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

  2. 所有引用基类的地方必须能透明地使用其子类的对象 (只要有父类出现的地方,都可以用子类来替代)

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

优势

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

缺点

  1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  2. 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  3. 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。

例子

  • 使用了里氏替换原则,将更基础的方法funcBase()提取出来,然后将A类和B类继承这个更基础的Base类,采用依赖、聚合或耦合的方式来减少父类和子类的耦合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

fun main(args: Array<String>) {
val a = A()
println("1 - 8 = ${a.func1(1,8)}")
println("----------------------------------------")
val b = B()
println("2 + 8 = ${b.func1(2,8)}")
println("1 + 8 + 9 = ${b.func2(1,8)}")
println("3 - 8 = ${b.func3(3,8)}")
println("----------------------------------------")

}
open class Base {
fun funcBase(num1: Int, num2: Int): Int {
//两个数的积
return num1 * num2
}
}

class A : Base() {
fun func1(num1: Int, num2: Int): Int {
// 两个数的差
return num1 - num2
}
}

class B : Base() {
private val a: A = A()
fun func1(num1: Int, num2: Int): Int {
//两个数相加
return num1 + num2
}

fun func2(num1: Int, num2: Int): Int {
//两个数相加,然后和 9求和
return func1(num1, num2) + 9
}

fun func3(num1: Int, num2: Int): Int {
//使用A的方法
return this.a.func1(num1, num2)
}
}