【设计模式】六原则(二)里式替换原则

本文最后更新于:2021年12月13日 下午

里式替换原则

1、定义

里式替换原则(Liskov Substitution Principle,LSP)表示: 所有引用基类的地方必须能够透明地使用其子类的对象。
换句话说就是,父类出现的地方可以用当前类的某一个子类来替代,反之则不然。

2、优点

1. 减少继承带来的缺点, 增强程序的健壮性, 在后续的升级改造中可以保持更好的兼容性;
2. 提高代码的复用性, 共性可以抽象为父类方法;
3. 提高代码的扩展性,由于子类可以包含但不限于父类的功能, 因此子类可以任意添加属于自己专属的功能。
4. 里式替换原则是指导设计父子类的设计原则。

3、示例

在原先的继承设计上存在这么一个问题, 如果一个父类存在多个子类,并且这些子类还使用了父类的同一个方法,
那么在对父类的方法进行修改时则需要同时考虑会不会影响子类,比如下面这个例子:
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
44
package cc.niushuai.study.designpattern.sixprinciples.lsp;

/**
* 里式替换原则
*
* @author niushuai
* @date 2021/10/18 13:48:48
*/

class Father {

public void print() {
System.out.println("i am father");
}
}

class Son extends Father {
@Override
public void print() {
System.out.println("i am son");
}
}

class Daughter extends Father {
@Override
public void print() {
System.out.println("i am daughter");
}
}

public class Main {

public static void main(String[] args) {

Father father = new Father();
father.print();

Son son = new Son();
son.print();

Daughter daughter = new Daughter();
daughter.print();
}
}

运行结果:

1
2
3
i am father
i am son
i am daughter

此处由于子类已经对父类的方法进行了覆写,因此子类出现的地方不可以替换为父类, 否则得到的结果就是一个子类输出的结果, 与父类无关。

4、里式替换原则的四个原则

1、子类必须实现父类的抽象方法,但不得重写父类的非抽象方法

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
package cc.niushuai.study.designpattern.sixprinciples.lsp.main2;

/**
* TODO
*
* @author niushuai
* @date 2021/10/18 14:15:05
*/

class Father {
public void run() {
System.out.println("father run...");
}
}

class Son extends Father {
@Override
public void run() {
System.out.println("son run...");
}
}

public class Main2 {

public static void main(String[] args) {

System.out.println("父类运行结果...");
Father father = new Father();
father.run();

System.out.println("子类运行结果...");
Son son = new Son();
son.run();
}
}

运行结果:

1
2
3
4
父类运行结果...
father run...
子类运行结果...
son run...

子类继承父类重写了run方法,然而子类的fly方法已经和父类不一致了,得到的结果与父类不同,
因此此处不能使用子类替换父类,违背了里式替换原则。

2、子类可以有自己的方法

子类源于父类, 因此具有父类的一切能具有的东西, 并且还可以扩展出属于自己的东西。

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
package cc.niushuai.study.designpattern.sixprinciples.lsp.main2;

/**
* TODO
*
* @author niushuai
* @date 2021/10/18 14:15:05
*/

class Father {
public void run() {
System.out.println("father run...");
}
}

class Son extends Father {

public void run2() {
System.out.println("son run 2...");
}
}

public class Main2 {

public static void main(String[] args) {

System.out.println("父类运行结果...");
Father father = new Father();
father.run();

System.out.println("子类运行结果...");
Son son = new Son();
son.run();

System.out.println("子类调用自己的方法结果...");
son.run2();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
父类运行结果...
father run...
子类运行结果...
father run...
子类调用自己的方法结果...
son run 2...

Process finished with exit code 0

可以看到 son.run()father.run()得到的结果一样, 并且 son.run2() 存在自己的处理逻辑, 符合里式替换原则。

3、当子类覆盖或实现父类的方法时,方法的输入参数可以比父类方法的输入参数更宽松

父类的方法形参类型为T,子类的形参为S,那么要求S>T
举例: 父类的方法形参为HashMap<String, Object> 子类的方法形参为Map<String, Object>

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
package cc.niushuai.study.designpattern.sixprinciples.lsp.main3;

import java.util.HashMap;
import java.util.Map;

/**
* TODO
*
* @author niushuai
* @date 2021/10/18 14:49:12
*/

class Father {

public void run(HashMap<String, Object> hashMap) {
System.out.println("父类 run...");
}
}

class Son extends Father {

// @Override 此处不能声明 override
public void run(Map<String, Object> map) {
System.out.println("子类 run...");
}
}

public class Main3 {

public static void main(String[] args) {

HashMap<String, Object> map = new HashMap<>();

System.out.println("父类结果");
Father father = new Father();
father.run(map);

System.out.println("子类结果");
Son son = new Son();
son.run(map);
}
}

运行结果:

1
2
3
4
父类结果
父类 run...
子类结果
父类 run...

我们可以看到子类的形参范围大于父类时, 并没影响到结果的输出 符合里式替换原则

4、当子类覆盖或实现父类的方法时,方法的返回结果可以比父类方法的返回结果范围更严格

父类的方法形参类型为T,子类的形参为S,那么要求S<T
举例: 父类的方法形参为Map<String, Object> 子类的方法形参为HashMap<String, Object>

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
package cc.niushuai.study.designpattern.sixprinciples.lsp.main4;

import java.util.HashMap;
import java.util.Map;

/**
* TODO
*
* @author niushuai
* @date 2021/10/18 15:04:30
*/

class Father {
public Map<String, Object> run() {

System.out.println("父类 run ...");
return new HashMap();
}
}

class Son extends Father {
@Override
public HashMap<String, Object> run() {

System.out.println("子类 run ...");
return new HashMap<>();
}
}

public class Main4 {

public static void main(String[] args) {

System.out.println("父类结果");
Father father = new Son();
father.run();

System.out.println("子类结果");
Son son = new Son();
son.run();
}
}

运行结果:

1
2
3
4
父类结果
子类 run ...
子类结果
子类 run ...

如果子类的方法形参为Map<String, Object> 父类的方法形参为HashMap<String, Object>是无法编译通过的,
因此这种方式的使用,子类覆写了则会调用子类的方法, 前提是父类的引用指向子类

里式替换原则 可能更偏向于子类不要刻意修改父类的实现, 可以存在自己的实现。