设计原则

2/25/2022 设计模式

摘要

JDK:1.8.0_202

# 一:单一职责原则(Single Responsibility Principle)

一个类负责一项职责

问题:比如一个类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障

解决方法:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能

扩展:虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2

比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)

# 1.1 v1.0

用一个类描述动物呼吸这个场景

class Animal1 {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸空气");
    }
}

public class Client1 {
    public static void main(String[] args) {
        Animal1 animal = new Animal1();
        animal.breathe("牛");    // 牛呼吸空气
        animal.breathe("羊");    // 羊呼吸空气
        animal.breathe("猪");    // 猪呼吸空气
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

问题:并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic

# 1.2 v2.0

class Terrestrial {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸空气");
    }
}

class Aquatic {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸水");
    }
}

public class Client2 {
    public static void main(String[] args) {
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");   // 牛呼吸空气
        terrestrial.breathe("羊");   // 羊呼吸空气
        terrestrial.breathe("猪");   // 猪呼吸空气

        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");       // 鱼呼吸水
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

问题:会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多

# 1.3 v3.0

class Animal2 {
    public void breathe(String animal) {
        if ("鱼".equals(animal)) {
            System.out.println(animal + "呼吸水");
        } else {
            System.out.println(animal + "呼吸空气");
        }
    }
}

public class Client3 {
    public static void main(String[] args) {
        Animal2 animal = new Animal2();
        animal.breathe("牛");    // 牛呼吸空气
        animal.breathe("羊");    // 羊呼吸空气
        animal.breathe("猪");    // 猪呼吸空气
        animal.breathe("鱼");    // 鱼呼吸水
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

问题:这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的

# 1.4 v4.0

class Animal3 {
    public void breathe(String animal) {
        System.out.println(animal + "呼吸空气");
    }

    public void breathe2(String animal) {
        System.out.println(animal + "呼吸水");
    }
}

public class Client4 {
    public static void main(String[] args) {
        Animal3 animal = new Animal3();
        animal.breathe("牛");    // 牛呼吸空气
        animal.breathe("羊");    // 羊呼吸空气
        animal.breathe("猪");    // 猪呼吸空气
        animal.breathe2("鱼");   // 鱼呼吸水
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。可以秉持的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则

# 1.5 小结

遵循单一职责原的优点有

  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多
  • 提高类的可读性,提高系统的可维护性
  • 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响

单一职责看似简单,实际上在实际运用过程中,会发现真的会出现很多职责扩展的现象,这个时候采用直接违反还会方法上遵循还是完全遵循单一职责原则还是取决于当前业务开发的人员的技能水平和这个需求的时间,如果技能水平不足,肯定会简单的if else 去解决,不会想什么原则,直接实现功能就好了,这也是为什么在很多小公司会发现代码都是业务堆起来的,当然也有好的小公司代码是写的好的,这个也是不可否认的。不过不管采用什么方式解决,心中至少要知道有几种解决方法

# 二:里氏替换原则(Liskov Substitution Principle)

继承与派生的规则

任何基类可以出现的地方,子类一定可以出现。里氏代换原则是对 "开-闭" 原则的补充。实现 "开-闭" 原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范

解剖描述: 定义1:如果类型T1可以用类型T2替换,那么类型T2是类型T1的子类型 定义2:所有引用基类的地方必须能透明地使用其子类的对象

通俗简单的说就是:子类可以扩展父类的功能,但不能改变父类原有的功能

问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障

解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。【有时候可以采用final的手段强制来遵循】

继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义

继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障

# 2.1 v1.0

一个两数相减的功能

class A {
    public int func1(int a, int b) {
        return a - b;
    }
}

public class Client1 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("100-50=" + a.func1(100, 50)); // 100-50=50
        System.out.println("100-80=" + a.func1(100, 80)); // 100-80=20
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

后来,需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:

  • 两数相减
  • 两数相加,然后再加100

# 2.2 v2.0

由于类A已经实现了第一个功能【两数相减】,所以类B继承类A后,只需要再完成第二个功能【两数相加,然后再加100】就可以了,代码如下:

class B extends A {
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 100;
    }
}

public class Client2 {
    public static void main(String[] args) {
        B b = new B();
        System.out.println("100-50=" + b.func1(100, 50));   // 100-50=150
        System.out.println("100-80=" + b.func1(100, 80));   // 100-80=180
        System.out.println("100+20+100=" + b.func2(100, 20));   // 100+20+100=220
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替

# 2.3 小结

再次来理解里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  • 子类中可以增加自己特有的方法
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。【注意区分重载和重写】
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格

# 三:依赖倒置原则(Dependence Inversion Principle)

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。即针对接口编程,不要针对实现编程

问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险

解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率

依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成

依赖倒置原则的核心思想是面向接口编程

# 3.1 v1.0

例子:母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了

class Book1 {
    public String getContent() {
        return "很久很久以前有一个阿拉伯的故事……";
    }
}

class Mother1 {
    public void narrate(Book1 book) {
        System.out.print("妈妈开始讲故事:");
        System.out.println(book.getContent());
    }
}

public class Client1 {
    public static void main(String[] args) {
        Mother1 mother = new Mother1();
        mother.narrate(new Book1()); // 妈妈开始讲故事:很久很久以前有一个阿拉伯的故事……
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

问题:上述是面向实现的编程,即依赖的是Book这个具体的实现类;看起来功能都很OK,也没有什么问题。假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

class Newspaper{
    public String getContent() {
        return "林书豪38+7领导尼克斯击败湖人……";
    }
}
1
2
3
4
5

这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行

# 3.2 v2.0

引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

public interface IReader {
    String getContent();
}
1
2
3

Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:

class Newspaper implements IReader {
    public String getContent() {
        return "林书豪17+9助尼克斯击败老鹰……";
    }
}

class Book2 implements IReader {
    public String getContent() {
        return "很久很久以前有一个阿拉伯的故事……";
    }
}

class Mother2 {
    public void narrate(IReader reader) {
        System.out.print("妈妈开始讲故事:");
        System.out.println(reader.getContent());
    }
}

public class Client2 {
    public static void main(String[] args) {
        Mother2 mother = new Mother2();
        mother.narrate(new Book2());    // 妈妈开始讲故事:很久很久以前有一个阿拉伯的故事……
        mother.narrate(new Newspaper());// 妈妈开始讲故事:林书豪17+9助尼克斯击败老鹰……
    }
}
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

这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。现在很流行的TDD开发模式就是依赖倒置原则最成功的应用

# 3.3 小结

传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生

在实际编程中,我们一般需要做到如下3点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。【可能会被人用到的】
  • 变量的声明类型尽量是抽象类或接口
  • 使用继承时遵循里氏替换原则

依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置

# 四:接口隔离原则(Interface Segregation Principle)

建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少

问题:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则

# 4.1 v1.0

interface I {
    void method1();

    void method2();

    void method3();

    void method4();

    void method5();
}

class A1 {
    public void depend1(I i) {
        i.method1();
    }

    public void depend2(I i) {
        i.method2();
    }

    public void depend3(I i) {
        i.method3();
    }
}

class B1 implements I {
    public void method1() {
        System.out.println("类B实现接口I的方法1");
    }

    public void method2() {
        System.out.println("类B实现接口I的方法2");
    }

    public void method3() {
        System.out.println("类B实现接口I的方法3");
    }

    // 对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
    // 所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method4() {
    }

    public void method5() {
    }
}

class C1 {
    public void depend1(I i) {
        i.method1();
    }

    public void depend2(I i) {
        i.method4();
    }

    public void depend3(I i) {
        i.method5();
    }
}

class D1 implements I {
    public void method1() {
        System.out.println("类D实现接口I的方法1");
    }

    // 对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
    // 所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method2() {
    }

    public void method3() {
    }

    public void method4() {
        System.out.println("类D实现接口I的方法4");
    }

    public void method5() {
        System.out.println("类D实现接口I的方法5");
    }
}


public class Client1 {
    public static void main(String[] args) {
        A2 a = new A2();
        a.depend1(new B2());    // 类B实现接口I1的方法1
        a.depend2(new B2());    // 类B实现接口I2的方法2
        a.depend3(new B2());    // 类B实现接口I2的方法3

        C2 c = new C2();
        c.depend1(new D2());    // 类D实现接口I1的方法1
        c.depend2(new D2());    // 类D实现接口I3的方法4
        c.depend3(new D2());    // 类D实现接口I3的方法5
    }
}
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

**问题:**接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分

# 4.2 v2.0

interface I1 {
    public void method1();
}

interface I2 {
    public void method2();

    public void method3();
}

interface I3 {
    public void method4();

    public void method5();
}

class A2 {
    public void depend1(I1 i) {
        i.method1();
    }

    public void depend2(I2 i) {
        i.method2();
    }

    public void depend3(I2 i) {
        i.method3();
    }
}

class B2 implements I1, I2 {
    public void method1() {
        System.out.println("类B实现接口I1的方法1");
    }

    public void method2() {
        System.out.println("类B实现接口I2的方法2");
    }

    public void method3() {
        System.out.println("类B实现接口I2的方法3");
    }
}

class C2 {
    public void depend1(I1 i) {
        i.method1();
    }

    public void depend2(I3 i) {
        i.method4();
    }

    public void depend3(I3 i) {
        i.method5();
    }
}

class D2 implements I1, I3 {
    public void method1() {
        System.out.println("类D实现接口I1的方法1");
    }

    public void method4() {
        System.out.println("类D实现接口I3的方法4");
    }

    public void method5() {
        System.out.println("类D实现接口I3的方法5");
    }
}

public class Client2 {
    public static void main(String[] args) {
        A2 a = new A2();
        a.depend1(new B2());    // 类B实现接口I的方法1
        a.depend2(new B2());    // 类B实现接口I的方法2
        a.depend3(new B2());    // 类B实现接口I的方法3

        C2 c = new C2();
        c.depend1(new D2());    // 类D实现接口I的方法1
        c.depend2(new D2());    // 类D实现接口I的方法4
        c.depend3(new D2());    // 类D实现接口I的方法5
    }
}
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

# 4.3 小结

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少

说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建

采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情

运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则

# 五:迪米特法则(Demeter Principle)

低耦合,高内聚

又叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。也就是说一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。

问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案:尽量降低类与类之间的耦合。

**通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。**迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部

# 5.1 v1.0

例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID

class Employee1 {
    private String id;

    public Employee1 setId(String id) {
        this.id = id;
        return this;
    }

    public String getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Employee1{" +
                "id='" + id + '\'' +
                '}';
    }
}

class SubCompanyManager1 {
    public List<Employee1> getAllEmployee() {
        return IntStream.range(0, 100).parallel().mapToObj(e -> new Employee1().setId("分公司:" + e)).collect(Collectors.toList());
    }
}

class CompanyManager1 {
    public List<Employee1> getAllEmployee() {
        return IntStream.range(0, 30).parallel().mapToObj(e -> new Employee1().setId("总公司:" + e)).collect(Collectors.toList());
    }

    public void printAllEmployee(SubCompanyManager1 sub) {
        sub.getAllEmployee().forEach(System.out::println);
        this.getAllEmployee().forEach(System.out::println);
    }
}


public class Client1 {
    public static void main(String[] args) {
        new CompanyManager1().printAllEmployee(new SubCompanyManager1());
    }
}
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

现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合

# 5.2 v2.0

class Employee2 {
    private String id;

    public Employee2 setId(String id) {
        this.id = id;
        return this;
    }

    public String getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Employee2{" + "id='" + id + '\'' + '}';
    }
}

class SubCompanyManager2 {
    public List<Employee2> getAllEmployee() {
        return IntStream.range(0, 100).parallel().mapToObj(e -> new Employee2().setId("分公司:" + e)).collect(Collectors.toList());
    }

    public void printEmployee() {
        this.getAllEmployee().forEach(System.out::println);
    }
}

class CompanyManager2 {
    public List<Employee2> getAllEmployee() {
        return IntStream.range(0, 30).parallel().mapToObj(e -> new Employee2().setId("总公司:" + e)).collect(Collectors.toList());
    }

    public void printAllEmployee(SubCompanyManager2 sub) {
        sub.printEmployee();
        this.getAllEmployee().forEach(System.out::println);
    }
}


public class Client2 {
    public static void main(String[] args) {
        new CompanyManager2().printAllEmployee(new SubCompanyManager2());
    }
}
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
45

修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合

# 5.3 小结

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个 "中介" 来发生联系,例如本例中,总公司就是通过分公司这个 "中介" 来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合

# 六:开闭原则(Open Close Principle)

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化

# 6.1 v1.0

例子:绘不同形状

class GraphicEditor1 {
    public void drawShape(int type) {
        switch (type) {
            case 1:
                drawRectangle();
                break;
            case 2:
                drawCircle();
                break;
            default:
                drawOther();
        }
    }

    private void drawRectangle() {
        System.out.println("这是矩形");
    }

    private void drawCircle() {
        System.out.println("这是圆形");
    }

    private void drawOther() {
        System.out.println("这是其他图形");
    }

}

public class Client1 {
    public static void main(String[] args) {
        GraphicEditor1 graphicEditor = new GraphicEditor1();
        graphicEditor.drawShape(1);    // 这是矩形
        graphicEditor.drawShape(2);    // 这是圆形
        graphicEditor.drawShape(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

问题:每增加一个功能都要需在使用方添加(case)代码快,过多的(case)会使代码过于臃肿,运行效率不高

# 6.2 v2.0

创建一个Shape类,并提供一个抽象的draw()方法,让子类实现该方法。每当增加一个图形种类时,让该图形种类继承Shape类,并实现draw()方法。这样,使用方只需编写一个drawShape方法,传入一个图形类的对象,即可使用其相应的绘图方法。

interface Shape {
    void draw();
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("这是矩阵");
    }
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("这是圆形");
    }
}

class Ohter implements Shape {
    @Override
    public void draw() {
        System.out.println("这是其他图形");
    }
}

class GraphicEditor2 {
    public void drawShape(Shape s) {
        s.draw();
    }
}

public class Client2 {
    public static void main(String[] args) {
        GraphicEditor2 graphicEditor = new GraphicEditor2();
        graphicEditor.drawShape(new Rectangle());    // 这是矩形
        graphicEditor.drawShape(new Circle());    // 这是圆形
        graphicEditor.drawShape(new Ohter());    // 这是其他图形
    }
}
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

# 七:组合/聚合复用原则(Composite Reuse Principle)

尽量使用组合和聚合少使用继承的关系来达到复用的原则

https://blog.51cto.com/u_15076233/4159169 https://blog.51cto.com/u_15127588/4512300

# 八:总结

◆ 开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭 ◆ 里氏替换原则告诉我们不要破坏继承体系 ◆ 依赖倒置原则告诉我们要面向接口编程 ◆ 单一职责原则告诉我们实现类要职责单一 ◆ 接口隔离原则告诉我们在设计接口的时候要精简单一 ◆ 迪米特法则告诉我们要降低耦合度 ◆ 组合/复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用

# 九:参考文献

最后更新: 3/11/2022, 3:24:56 PM