里氏替换原则(Liskov Substitution Principle)
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T
通俗来说,就是引用基类的的地方必须能够透明的使用其子类,透明的意思是说,将基类替换为子类,程序的行为不发生任何变化。但是将使用子类的地方替换为基类,可能会不行。
里氏替换原则定义了良好的继承模式:
- 子类必须完全实现父类的方法
- 子类可以有自己的属性或方法(这就导致了反向使用里氏替换原则可能是不行的)
- 覆盖父类的方法时参数可以放大(子类覆盖父类方法时,参数类型必须包含父类方法的参数类型)
- 覆盖父类的方法时输出可以缩小(子类覆盖父类方法时,返回值类型可以是父类方法返回值的子类型)
第1、2条是显而易见的,后两条有些难以理解,先说第三条,输入参数可以放大。
1class A {
2 foo(a: string) {
3
4 }
5}
6
7class B extends A {
8 foo(a: string | number) {
9 if (typeof a === "string") {
10 super.foo(a);
11 } else {
12 // ...
13 }
14 }
15}
上述代码中,foo方法的参数由string放大为string|number,使用class A的地方完全能够使用class B代替,这样的class就满足里氏替换原则,其行为也是健壮的。参数类型放大,说明你的子类的功能被扩展了,但同时也要确保参数放大后,在相同类型的情况下,子类方法与父类方法的行为是一致的。
对于第四条,里氏替换原则要求子类方法返回值的类型小于等于父类方法返回值,仍以ts为例:
1class A {
2 foo(a: string): HTMLElement {
3 return new HTMLElement();
4 }
5}
6
7class B extends A {
8 foo(a: string | number): HTMLDivElement {
9 if (typeof a === "string") {
10 super.foo(a);
11 } else {
12 // ...
13 }
14 return new HTMLDivElement();
15 }
16}
class A中foo方法返回值类型为HTMLElement,class B中foo方法返回值类型为HTMLDivElement,而HTMLDivElement是HTMLElement的子类,这样的继承关系就是符合里氏替换原则的,因为其核心概念是透明,如果子类方法返回值类型大于父类,那么用到该返回值的地方则有可能调用一个在父类方法返回值中不存在方法或属性,在将父类替换为子类时程序很有可能会报错。
幸运的是在ts中,子类覆盖父类方法时,如果返回值类型大于父类的,ts的类型系统会直接报错,类型系统保证了我们写不出返回值类型放大的代码,当然,你要是用any的话那当我没说。