之前或多或少接触过几种设计模式,只不过没有好好的总结。本文用于记录自己在设计模式方面的学习过程,在学习期间阅读了有关设计模式的书籍,挑选了博客园中优秀的文章,搜集网上的资料并进行了筛选,也看过别人对设计模式的总结。只是单纯看的话,会容易忘记。因此,我一般喜欢“输出式学习”,将学过的知识与自己对知识的理解整理下来,构建一套属于自己的知识体系。即便以后忘记了,也会通过浏览对原有知识的总结,很快重新拾起这部分的知识。

概述

“前人栽树,后人乘凉”,下图罗列出了与设计模式相关的所有内容,包括设计原则创建型模式构建型模式以及结构型模式这四个部分。其中,设计原则是设计模式的基础,是程序员在编码时应遵守的原则,它解释了设计模式为什么要这么设计,同时也是设计模式的依据;而后三个部分是对设计模式的分类,根据不同的设计模式所实现的方式、呈现的不同特点、执行的不同行为所划分出不同的类别。

红色内容表示常用设计模式。

  • 设计原则
    • 单一职责原则
    • 里氏替换原则
    • 依赖倒置原则
    • 接口隔离原则
    • 迪米特法则
    • 开闭原则
  • 创建型的设计模式
    • 单例模式
    • 工厂方法模式
    • 抽象工厂模式
    • 建造者模式
    • 原型模式
  • 结构型的设计模式
    • 代理模式
    • 适配器模式
    • 桥接模式
    • 装饰模式
    • 组合模式
    • 外观模式
    • 享元模式
  • 行为型的设计模式
    • 模板方法模式
    • 责任链模式
    • 策略模式
    • 迭代器模式
    • 观察者模式
    • 状态模式
    • 命令模式
    • 中介者模式
    • 备忘录模式
    • 解释器模式
    • 访问者模式

图片大小约为 4.6M,由于网络原因可能会出现加载过慢的情况,请耐心等待。

下面分别对以上内容进行展开。

设计原则

单一职责原则

从字面意思上看,单一职责原则(Single Responsibility Principle,SRP)就是对于接口、方法、类来说,必须保证只有一个引起它变化的原因,否则类应该被拆分。如果一个类有一个以上的职责,这些职责就耦合在一起了,这就不满足单一职责的设计,这就会造成当一个职责发生变化的时候,可能会影响其它职责。此外,多个职责耦合在一起会影响其复用性。

基于以上的说明,你可以看到单一职责原则其实又称为单一功能原则,其核心就是控制类的粒度大小、将对象解耦、提高其内聚性。你可以想象一下,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。此外,单一职责原则也适用于方法,一个方法应该尽可能做好一件事情,如果一个方法处理的事情太多,那么其粒度就会变得很粗,不利于重用。

看下一个不满足单一职责原则的例子,首先声明了一个交通工具类 Vehicle,其中的 run() 方法接收不同类型的交通工具,而在主函数中创建了三个不同的对象,它们都使用了同一个 run() 方法,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class _01_1_SingleResponsibilityPrinciple {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("汽车");
        vehicle.run("飞机");
        vehicle.run("潜艇");
    }
}

class Vehicle {
    public void run (String vehicle) {
        System.out.println(vehicle + " 在公路上运行...");
    }
}

输出结果如下:

1
2
3
汽车 在公路上运行...
飞机 在公路上运行...
潜艇 在公路上运行...

从结果可以看到,飞机 在公路上运行...以及潜艇 在公路上运行...这显然是不合适的,这是因为它们都使用了 Vehicle 中的 run() 方法,这里了违反了单一职责原则。解决方法也很简单,可以根据交通工具运行方式的不同,分解成不同的类即可。如下所示:

 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
public class _01_2_SingleResponsibilityPrinciple {
    public static void main(String[] args) {
        RoadVehicle roadVehicle = new RoadVehicle();
        roadVehicle.run("汽车");

        AirVehicle airVehicle = new AirVehicle();
        airVehicle.run("飞机");

        WaterVehicle waterVehicle = new WaterVehicle();
        waterVehicle.run("潜艇");
    }
}

class RoadVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在路上跑...");
    }
}

class AirVehicle {
    public void run (String vehicle) {
        System.out.println(vehicle + " 在空中飞...");
    }
}

class WaterVehicle {
    public void run (String vehicle) {
        System.out.println(vehicle + " 在水里游...");
    }
}

输出结果如下:

1
2
3
汽车 在路上跑...
飞机 在空中飞...
潜艇 在水里游...

可以看到,以上的方式是可以满足单一职责原则的,即每个类都做自己分内的事。但需要注意的是,以上的方式在修改的过程中,它的改动是很大的,即将类进行了分解,又修改了客户端(例如 main 方法中创建对象的过程)。因此,还可以对第一种方法中的 Vehicle 类修改为以下的形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class _01_3_SingleResponsibilityPrinciple {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.runRoad("汽车");
        vehicle.runAir("飞机");
        vehicle.runWater("潜艇");
    }
}

class Vehicle {
    public void runRoad(String vehicle) {
        System.out.println(vehicle + " 在路上跑...");
    }

    public void runAir(String vehicle) {
        System.out.println(vehicle + " 在空中飞...");
    }

    public void runWater(String vehicle) {
        System.out.println(vehicle + " 在水里游...");
    }
}

输出结果如下:

1
2
3
汽车 在路上跑...
飞机 在空中飞...
潜艇 在水里游...

可以看到,以上修改方式虽然也违背了单一职责原则,对原来的类做了修改,但只是增加了几个方法。从方法层面上看,它是符合单一职责原则的,因为没有改动原来方法中的代码,每个方法分别有自己的职责,完成自己的任务,不干涉其它方法的任务。

最后,需要注意的是:通常情况下,应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责原则(就像上述对类的修改那样,添加了几个方法)。只有类中方法数量足够少的时候,可以在方法级别保证单一职责原则

里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)与继承相关,即在子类继承父类的时候,必须保证父类所拥有的性质在子类中仍然成立。也就是说,子类可以扩展父类的功能,但不能改变父类原有的功能,除了添加新的方法用于完成新功能之外,尽量不要重写父类中的方法。这是因为,当通过重写父类中的方法来完成新的功能时,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,在使用多态特性的场景下十分明显。

里氏替换原则事实上主要阐述了与继承有关的一些原则,即什么时候应该使用继承,怎么使用继承,使用继承的时候需要注意的地方。该原则反应了子类和父类之间的关系,是开闭原则的补充。

回顾继承的概念可以发现,继承在最初设计的时候,能够带来许多便利,但也带来了一些弊端。例如,使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象之间的耦合性。耦合性体现在:如果一个类 A 都被其它类继承,那么当类 A 需要修改的时候,必须考虑到所有的子类,并且类 A 修改后,所有涉及到的子类中的功能都有可能发生故障。

因此,里氏替换原则告诉我们:我们在进行编码的时候应尽量做到高内聚、低耦合,而继承实际上让类与类之间的耦合性提高了,所以,可以在适当的情况下通过聚合组合依赖的方式来解决问题。

例如,类 C 继承了类 B,类 B 继承了类 A,那么我们可以让类 C 去继承类 A,将其中某些通用的方法更加抽象地在类 A 中实现,这样的话,类 C 和类 B 之间就没有耦合关系了。

请看以下示例:

 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
public class _02_1_LiskovSubstitutionPrinciple {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("3 - 1 = " + a.func1(3, 1));
        System.out.println("8 - 2 = " + a.func1(8, 2));

        System.out.println("==============");

        B b = new B();
        System.out.println("3 - 1 = " + b.func1(3, 1));
        System.out.println("8 - 2 = " + b.func1(8, 2));
        System.out.println("3 + 1 + 10 = " + b.func2(3, 1));
    }
}

class A {
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

class B extends A {
    public int func1(int value1, int value2) {
        return value1 + value2;
    }

    public int func2(int value1, int value2) {
        return func1(value1, value2) + 10;
    }
}

输出结果如下:

1
2
3
4
5
6
3 - 1 = 2
8 - 2 = 6
==============
3 - 1 = 4
8 - 2 = 10
3 + 1 + 10 = 14

先不着急看运行结果,我们先看一下类 A 和类 B 所实现的功能。类 A 中的 func1() 方法用于返回两数之差,类 B 继承了类 A,但是由于需要在类 B 中新添加一个功能:即完成两数相加,然后再和 10 求和的功能。也就是说,类 B 无意间重写了类 A 中的 func1() 方法。

从结果中可以看出,分割线上面的输出结果是正确的,因为都是调用了类 A 中的 func1() 方法。而分割线下面的输出结果是错误的。我的本意是求出3-18-2的结果,而由于子类 B 进行了重写(我可能没注意到),其实执行的是求和逻辑,也就违背了我想要执行求差的逻辑。因此,以上代码违背了里氏替换原则

如何解决这种问题呢

通过示例可以看出,原先运行正常的求差功能出现了错误,原因是类 B 无意间重写了父类的方法。因此,可以让原来的父类和子类都继承一个更加通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合等关系代替。如下所示:

 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
public class _02_2_LiskovSubstitutionPrinciple {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("3 - 1 = " + a.func1(3, 1));
        System.out.println("8 - 2 = " + a.func1(8, 2));

        System.out.println("==============");

        B b = new B();
        // 由于类 B 不再继承类 A,因此调用者就会很明确该函数完成的功能
        System.out.println("3 + 1 = " + b.func1(3, 1));
        System.out.println("8 + 2 = " + b.func1(8, 2));
        System.out.println("3 + 1 + 10 = " + b.func2(3, 1));

        System.out.println("==============");
        System.out.println("3 - 1 = " + b.func3(3, 1));
    }
}

class Base {
    // 把更加基础的方法和成员放到 Base 类中
}

class A extends Base {
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

class B extends Base {
    // 使用到了组合关系
    private A a = new A();

    public int func1(int value1, int value2) {
        return value1 + value2;
    }

    public int func2(int value1, int value2) {
        return func1(value1, value2) + 10;
    }

    // 调用类 A 中的求差方法
    public int func3(int value1, int value2) {
        return a.func1(value1, value2);
    }
}

输出结果如下所示:

1
2
3
4
5
6
7
8
3 - 1 = 2
8 - 2 = 6
==============
3 + 1 = 4
8 + 2 = 10
3 + 1 + 10 = 14
==============
3 - 1 = 2

可以看到,类 A 和类 B 都继承了基类 Base,现在类 A 中的 func1() 方法就很明确了,就是专门求差的方法。而类 B 中的方法也很明确,就是专门用来求和的方法。此外,如果类 B 想要用到类 A 中的方法,那么直接在类 B 中创建对象即可。并且,还可以使用类 A 中的 fun1() 方法,用于求两数之差。

通过以上的示例可以看到,里氏替换原则所说的就是子类可以扩展父类的功能,但不能改变父类原有的功能。

依赖倒置原则

依赖倒置原则(Dependence Inversion Principe,DIP)指高层模块不应该依赖低层模块,两者都应该以来其抽象。模块之间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

抽象在这里指的是接口或者抽象类,细节指的是具体的实现类。因此,该原则指出抽象不应该依赖细节,而细节应该依赖抽象。换句话说,就是接口或者抽象类不应该依赖实现类,而实现类应该依赖接口或者抽象类。核心在于:要面向接口编程,而不是面向实现编程。

为什么这么说?这是因为在软件设计中,具体的细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。使用接口或者抽象类的目的就是制定好规范和标准,而不去涉及任何具体的操作,而把展现细节的任务交给它们的实现类去完成。

看一下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class _03_1_DependenceInversionPrincipe {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
    }
}

class Email {
    public String getInfo() {
        return "邮件信息:Hello...";
    }
}

class Person {
    public void receive(Email email) {
        System.out.println(email.getInfo());
    }
}

输出结果如下所示:

1
邮件信息:Hello...

以上示例实现的功能是能够让 Person 类接收消息。实现起来很简单,但这里只实现了接收邮箱 Email 的消息,而如果我们想要实现让 Person 接收短信消息、微信消息的话,则需要新添加短信类或者微信类,同时 Person 类也需要增加对应的方法才可以。你可以看到 Person 类中的 receive() 方法和 Email 类耦合在了一起,不利于以后业务的扩展。那如何解决呢?

可以引入一个接口 IReceiver 表示接收者,让邮箱、微信、短信都各自实现该接口即可。如下所示:

 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
public class _03_2_DependenceInversionPrincipe {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
        person.receive(new WeChat());
    }
}

interface IReceiver {
    String getInfo();
}

class Email implements IReceiver {

    @Override
    public String getInfo() {
        return "邮件信息:Hello...";
    }
}

class WeChat implements IReceiver {

    @Override
    public String getInfo() {
        return "微信消息:Hi...";
    }
}

class Person {
    // receive 依赖了接口 IReceiver
    public void receive(IReceiver receiver) {
        System.out.println(receiver.getInfo());
    }
}

输出结果如下:

1
2
邮件信息:Hello...
微信消息:Hi...

可以看到,在以上的示例中定义了一个接口 IReceiver,其中的 Email 和 WeChat 类都实现了该接口,并且都在自己的类中重写了自己的方法。Person 类中只依赖了接口 IReceiver,而没有依赖具体的实现类(如 Email)。

因此,依赖倒置原则的目的是通过面向接口编程的方式来降低类间的耦合性,在使用该原则的时候,应给每个类尽量提供接口或抽象类,或者两者都具备。

接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)字面意思上理解是尽量将多个接口或者复杂的接口拆分成更小并且更具体的接口。要为每个类建立它们需要的专用接口,而不是试图去建立一个复杂而庞大的接口供所有依赖它的类去调用。

我自己的理解是这样的,比如说类 A 由于需要使用到接口 Ⅰ 的部分方法(注意不是全部的方法)而实现了该接口,但是类 A 不得不需要重写接口 Ⅰ 中的所有方法,但类 A 只是需要其中的几个方法而却重写了接口中的全部方法,这对于类 A 来说无异于显得有些臃肿。所以,可以将接口 Ⅰ 拆分成多个接口,例如先将类 A 需要的方法拆分出来,封装成一个接口,然后将接口 Ⅰ 剩下的接口再放到一起,供其它类使用。

看以下示例,该示例展示的是定义了一个接口,在该接口中一共含有 5 个方法,此外,类 A 通过该接口依赖了类 B,类 C 通过该接口依赖了类 D。

 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
public class _04_1_InterfaceSegregationPrinciple {
    public static void main(String[] args) {
    }
}

interface Interface1 {
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
}

class B implements Interface1 {

    @Override
    public void method1() { System.out.println("类 B 中实现了 method1()..."); }

    @Override
    public void method2() { System.out.println("类 B 中实现了 method2()..."); }

    @Override
    public void method3() { System.out.println("类 B 中实现了 method3()..."); }

    @Override
    public void method4() { System.out.println("类 B 中实现了 method4()..."); }

    @Override
    public void method5() { System.out.println("类 B 中实现了 method5()..."); }
}

class D implements Interface1 {

    @Override
    public void method1() { System.out.println("类 D 中实现了 method1()..."); }

    @Override
    public void method2() { System.out.println("类 D 中实现了 method2()..."); }

    @Override
    public void method3() { System.out.println("类 D 中实现了 method3()..."); }

    @Override
    public void method4() { System.out.println("类 D 中实现了 method4()..."); }

    @Override
    public void method5() { System.out.println("类 D 中实现了 method5()..."); }
}

// 类 A 通过接口 Interface1 依赖了 B 类,但只使用了 method1()、method2()、method3() 三个方法
class A {
    public void dependence1(Interface1 interface1) {
        interface1.method1();
    }

    public void dependence2(Interface1 interface1) {
        interface1.method2();
    }

    public void dependence3(Interface1 interface1) {
        interface1.method3();
    }
}

// 类 C 通过接口 Interface1 依赖了 D 类,但只使用了 method1()、method4()、method5()三个方法
class C {
    public void dependence1(Interface1 interface1) {
        interface1.method1();
    }

    public void dependence4(Interface1 interface1) {
        interface1.method4();
    }

    public void dependence5(Interface1 interface1) {
        interface1.method5();
    }
}

你可以看到,类 B 和类 D 都实现了接口 Interface1 中的方法,但是类 A 只用到了其中的三个方法,类 C 也只用到了其中的是哪个方法。即使是用到了三个方法,就需要将全部的方法进行实现,这无异于显得有些累赘。因此,根据接口隔离原则可以将该接口拆分成独立的几个接口,让类 A 和类 C 分别与它们需要的接口建立依赖关系即可。接口的分解如下所示:

  • Interface1:
    • method1()
  • Interface2:
    • method2()
    • method3()
  • Interface3:
    • method4()
    • method5()

如下所示:

 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
public class _04_1_InterfaceSegregationPrinciple {
    public static void main(String[] args) {
        A a = new A();
        // 类 A 通过接口依赖类 B
        a.dependence1(new B());
        a.dependence2(new B());
        a.dependence3(new B());

        C c = new C();
        // 类 C 通过接口依赖类 D
        c.dependence1(new D());
        c.dependence4(new D());
        c.dependence5(new D());
    }
}

interface Interface1 {
    void method1();
}

interface Interface2 {
    void method2();
    void method3();
}

interface Interface3 {
    void method4();
    void method5();
}

class B implements Interface1, Interface2 {

    @Override
    public void method1() { System.out.println("类 B 中实现了 method1()..."); }

    @Override
    public void method2() { System.out.println("类 B 中实现了 method2()..."); }

    @Override
    public void method3() { System.out.println("类 B 中实现了 method3()..."); }
}

class D implements Interface1, Interface3 {

    @Override
    public void method1() { System.out.println("类 D 中实现了 method1()..."); }

    @Override
    public void method4() { System.out.println("类 D 中实现了 method4()..."); }

    @Override
    public void method5() { System.out.println("类 D 中实现了 method5()..."); }
}

// 类 A 通过接口 Interface1、Interface2 依赖了 B 类,使用到了 method1()、method2()、method3() 三个方法
class A {
    public void dependence1(Interface1 interface1) { interface1.method1(); }

    public void dependence2(Interface2 interface2) { interface2.method2(); }

    public void dependence3(Interface2 interface2) { interface2.method3(); }
}

// 类 C 通过接口 Interface1、Interface3 依赖了 D 类,使用到了 method1()、method4()、method5()三个方法
class C {
    public void dependence1(Interface1 interface1) { interface1.method1(); }

    public void dependence4(Interface3 interface3) { interface3.method4(); }

    public void dependence5(Interface3 interface3) { interface3.method5(); }
}

输出结果如下:

1
2
3
4
5
6
 B 中实现了 method1()...
 B 中实现了 method2()...
 B 中实现了 method3()...
 D 中实现了 method1()...
 D 中实现了 method4()...
 D 中实现了 method5()...

通过改进可以看出,这里对之前的接口进行了改进,将其分解成多个接口,尽量做到接口的细化,接口中的方法尽量少,为每个类设计专用的接口。

接口隔离原则与之前的单一职责原则都是为了提高类的内聚性、降低它们之间的耦合性,都体现了封装的思想,不同点在于:

  • 前者注重对接口的依赖隔离,后者注重的是职责
  • 前者主要对接口进行约束,针对抽象和程序整体框架的构建,后者主要对进行约束,针对的是程序中的实现和细节。

迪米特法则

迪米特法则(Law of Demeter,LoD)又称最少知道原则,指的是一个对象应该对其它对象保持最少的了解。也就是说,一个类对自己所依赖的类知道的越少越好。例如,类 A 依赖了类 B,那么对于被依赖的类 B 来说,不管它的内部再怎么复杂,都尽量将其逻辑封装在类的内部,除了对外提供公共的 public 方法以外,不对外泄漏任何信息。

可以将以上的内容概括为:只与直接朋友通信。这里的直接朋友指的是:一般情况下,每个对象都会与其它对象建立耦合关系(例如,依赖、关联、组合、聚合),只要两个对象之间存在耦合关系,就称这两个对象之间是朋友关系。这里将成员变量、方法参数、方法返回值中的类称为直接朋友,而出现在局部变量中的类不是直接朋友。因此,对于那些陌生的类来说,最好不要以局部变量的形式出现在类的内部。

下面看一个违反迪米特法则的示例,该示例表示的是有一个学校,其下属部门有各个学院以及学校的总部,现要求打印出学校总部员工的 ID 和学院员工的 ID。

 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
public class _05_1_LawOfDemeter {
    public static void main(String[] args) {
        SchoolManager manager = new SchoolManager();
        manager.printAllEmployee(new CollegeManager());
    }
}

// 学校总部的员工类
class Employee {
    private String id;

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

    public String getId() {
        return id;
    }
}

// 学院的员工类
class CollegeEmployee {
    private String id;

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

    public String getId() {
        return id;
    }
}

// 用于管理学院员工的类
class CollegeManager {
    // 返回学院所有的员工 id
    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("学院员工 id = " + i);
            list.add(emp);
        }
        return list;
    }
}

// 用于管理学校的类
class SchoolManager {
    // 返回学校总部的所有员工的 id
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Employee emp = new Employee();
            emp.setId("学校总部员工 id = " + i);
            list.add(emp);
        }
        return list;
    }

    // 用于输出学院员工 id 和学校总部员工 id
    public void printAllEmployee(CollegeManager manager) {
        // CollegeEmployee 属于局部变量,
        // 因此违反了迪米特法则
        List<CollegeEmployee> list1 = manager.getAllEmployee();
        System.out.println("========学院员工 id ========");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }

        List<Employee> list2 = this.getAllEmployee();
        System.out.println("========学校总部员工 id ========");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }
}

输出结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
========学院员工 id ========
学院员工 id = 0
学院员工 id = 1
学院员工 id = 2
学院员工 id = 3
学院员工 id = 4
学院员工 id = 5
学院员工 id = 6
学院员工 id = 7
学院员工 id = 8
学院员工 id = 9
========学校总部员工 id ========
学校总部员工 id = 0
学校总部员工 id = 1
学校总部员工 id = 2
学校总部员工 id = 3
学校总部员工 id = 4

main 函数中首先创建了一个 SchoolManager 对象,然后调用了其 printAllEmployee() 方法用于输出学院员工的 id 以及学校总部员工的 id。

这里以 SchoolManager 类为例,分析其直接朋友有哪些。由于 Employee 属于方法的返回值,因此属于 SchoolManager 类的直接朋友。然后 CollegeManager 也属于 SchoolManager 类的直接朋友,因为它位于方法的参数上。但需要注意的是,CollegeEmployee 不是直接朋友,因为它不属于 SchoolManager 类的成员变量,也不属于方法参数,更不属于方法的返回值。因此,CollegeEmployee 违反了迪米特法则。那么如何改进呢?

之前说到:**类 A 依赖了类 B,那么对于被依赖的类 B 来说,不管它的内部再怎么复杂,都尽量将其逻辑封装在类的内部,除了对外提供公共的 public 方法以外,不对外泄漏任何信息。**因此,我们可以将 printAllEmployee() 方法中获取学院员工 id 的部分代码封装到 CollegeManager 类中,然后在 printAllEmployee() 方法中调用即可。如下所示:

 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
class CollegeManager {
    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("学院员工 id = " + i);
            list.add(emp);
        }
        return list;
    }

    // 新添加的方法
    public void printEmployee() {
        List<CollegeEmployee> list1 = this.getAllEmployee();
        System.out.println("========学院员工 id ========");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
    }
}

class SchoolManager {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Employee emp = new Employee();
            emp.setId("学校总部员工 id = " + i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(CollegeManager manager) {
        // 直接调用即可
        manager.printEmployee();

        List<Employee> list2 = this.getAllEmployee();
        System.out.println("========学校总部员工 id ========");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }
}

这里将输出学院员工 id的代码封装到了 printEmployee() 方法中,并且该方法是 public 的,可以对外使用。因此,在 printAllEmployee() 方法中直接调用了 printEmployee() 方法,此时便符合了迪米特法则

该法则强调的是降低类与类之间的耦合性,但需要注意的是,由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间耦合,并不是要求完全没有依赖关系。

开闭原则

开闭原则(Open Closed Principle,OCP)一句话总结:对扩展开放,对修改关闭。这里所操作的实体可以是项目中的模块,或者类、接口、方法。由于可以扩展模块的功能,因此该原则可以提高软件的可复用性以及可维护性。

**用抽象构建框架,用实现扩展细节。**这是开闭原则的实现方式。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中容易变化的细节可以通过具体的实现类进行扩展。也就是说,当软件需要变化的时候,尽量通过扩展软件实体的方式来实现变化,而不是通过修改已有的代码来实现变化。

看如下示例,该示例通过创建不同的对象,输出不同的绘制图形的形状。

 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
public class _06_1_OpenClosedPrinciple {
    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.drawShape(new Rectangle());
        editor.drawShape(new Circle());
    }
}

class GraphicEditor {
    public void drawShape(Shape s) {
        if (s.type == 1) {
            drawRectangle(s);
        } else if (s.type == 2) {
            drawCircle(s);
        }
    }

    public void drawRectangle(Shape s) {
        System.out.println("绘制矩形");
    }

    public void drawCircle(Shape s) {
        System.out.println("绘制圆形");
    }
}

class Shape {
    int type;
}

class Rectangle extends Shape {
    Rectangle() {
        type = 1;
    }
}

class Circle extends Shape {
    Circle() {
        type = 2;
    }
}

输出结果如下:

1
2
绘制矩形
绘制圆形

在 main 函数中,通过 GraphicEditor 的 drawShape() 接收不同的对象,根据每个对象中的 type 来判断其调用的具体方法。这种实现方式比较简单,易于理解。但如果想要再扩展新的功能的话,那么修改的地方就比较多了。例如,想要新添加一个图形的种类,将会进行如下修改:

 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
public class _06_1_OpenClosedPrinciple {
    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.drawShape(new Rectangle());
        editor.drawShape(new Circle());

        // 绘制三角形
        editor.drawShape(new Triangle());
    }
}

class GraphicEditor {
    public void drawShape(Shape s) {
        if (s.type == 1) {
            drawRectangle(s);
        } else if (s.type == 2) {
            drawCircle(s);
            // 这里增加了判断
        } else if (s.type == 3) {
            drawTriangle(s);
        }
    }

    public void drawRectangle(Shape s) {
        System.out.println("绘制矩形");
    }

    public void drawCircle(Shape s) {
        System.out.println("绘制圆形");
    }

    // 这里增加了方法
    public void drawTriangle (Shape s) {
        System.out.println("绘制三角形");
    }
}

class Shape {
    int type;
}

class Rectangle extends Shape {
    Rectangle() {
        type = 1;
    }
}

class Circle extends Shape {
    Circle() {
        type = 2;
    }
}

// 这里增加了新的类
class Triangle extends Shape {
    Triangle() {
        type = 3;
    }
}

输出结果如下:

1
2
3
绘制矩形
绘制圆形
绘制三角形

上面的修改就违反了开闭原则,那么如何解决呢?这里可以通过将 Shape 类构建成抽象类,然后提供一个抽象的 draw() 方法,让子类去实现该方法即可。如下所示:

 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
public class _06_1_OpenClosedPrinciple {
    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.drawShape(new Rectangle());
        editor.drawShape(new Circle());

        editor.drawShape(new Triangle());
    }
}

class GraphicEditor {
    public void drawShape(Shape s) {
        // 直接调用 draw() 方法
       s.draw();
    }
}

// 抽象类
abstract class Shape {
    int type;

    // 抽象方法
    public abstract void draw();
}

class Rectangle extends Shape {
    Rectangle() {
        type = 1;
    }

    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

class Circle extends Shape {
    Circle() {
        type = 2;
    }

    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

class Triangle extends Shape {
    Triangle() {
        type = 3;
    }

    @Override
    public void draw() {
        System.out.println("绘制三角形");
    }
}

输出结果还是一样的,这里将 Shape 类用 abstract 修饰,然后提供了一个公共的抽象方法 draw(),在 GraphicEditor 类中直接调用了该方法。如果再次扩展功能的话,直接再新添加一个类即可,例如画五角星图形的类,让它继承 Shape 类,而 Shape 类是不用修改的。

创建型模式(5 种)

创建型模式主要关注的是“如何创建对象”,即将对象的创建与使用进行分离,降低系统耦合度的同时无需关心对象的创建细节,对象的创建由相关的工厂来完成。

对于以下 5 种创建型模式,除了工厂方法模式属于类创建型,其它的方法全部属于对象创建型

单例模式

单例模式(Singleton)在之前的文章中提到过,即 《Java 单例模式[备忘]》,其思想为:保证一个类只有一个实例,并提供一个访问该示例的全局访问点,也就是对外提供一个 public 方法即可。

通常情况下,普通类的构造函数是 public 的,外部类可通过 new 关键字来生成多个实例。但如果将类的构造函数设置为 private 的,则外部类就无法调用该构造函数,也就无法生成多个实例了。因此,如果想要满足单例模式,该类自身就需要定义一个静态私有实例,然后向外提供一个静态的共有(public)函数,用于创建或获取该静态私有的实例。

详细内容请见 《Java 单例模式[备忘]》一文。

工厂方法模式

工厂方法(FactoryMethod)模式通过定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类中完成。其中,被创建出来的东西称为产品,创建产品的东西称为工厂。如果要创建的产品不多,只需要一个工厂类就可以完成的话,这种称为简单工厂模式

在介绍简单工厂模式之前,先通过以下示例展示在不使用设计模式的情况下,如何实现以下需求。在该示例中,我们的需求是要生产披萨,而成产披萨的环节有准备原材料(prepare)、烘焙(bake)、将披萨切开(cut)以及装盒(box)四个操作,其中可以生产 CheesePizza 和 GreekPizza 两种披萨。如下所示:

 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
// Pizza.java
public abstract class Pizza {
    public String name;

    public abstract void prepare();

    public void back() {
        System.out.println(name + " backing...");
    }

    public void cut() {
        System.out.println(name + " cutting...");
    }

    public void box() {
        System.out.println(name + " boxing...");
    }

    public void setName(String name) {
        this.name = name;
    }
}

// GreekPizza.java
public class GreekPizza extends Pizza {
    @Override
    public void prepare() {
        System.out.println("给 GreekPizza 准备原材料...");
    }
}

// CheesePizza.java
public class CheesePizza extends Pizza {
    @Override
    public void prepare() {
        System.out.println("给 CheessPizza 准备原材料...");
    }
}

// OrderPizza.java
// 用于订购披萨
public class OrderPizza {

    public OrderPizza() {
        Pizza pizza = null;
        String orderType;

        do {
            orderType = getType();
            if (orderType.equals("greek")) {
                pizza = new GreekPizza();
                pizza.setName("GreekPizza");
            } else if (orderType.equals("cheese")) {
                pizza = new CheesePizza();
                pizza.setName("CheesePizza");
            } else {
                break;
            }

            // 输出披萨的制作过程
            pizza.prepare();
            pizza.back();
            pizza.cut();
            pizza.box();
        } while (true);
    }

    private String getType() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入披萨类型:");
            String str = reader.readLine();
            return str;
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }
}

// PizzaStore.java
// 相当于客户端,即用于发出订购披萨的订单
public class PizzaStore {
    public static void main(String[] args) {
        new OrderPizza();
    }
}

输出结果如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
请输入披萨类型:
greek
给 GreekPizza 准备原材料...
GreekPizza backing...
GreekPizza cutting...
GreekPizza boxing...
请输入披萨类型:
abc

Process finished with exit code 0

以上代码流程为:

  • 首先定义了一个抽象的 Pizza 类,并声明了四个制作披萨的过程;
  • 其次又定义了 CheesePizza 和 GreekPizza,都继承了 Pizza 类;然后定义了 OrderPizza 类,用于订购披萨,在该类中又根据输入不同的披萨类型进而创建不同的对象,但由于只声明了两种类型的披萨,如果输入的是其它类型的披萨,则会执行 break,即退出 while 循环;
  • 最后定义了 PizzaStore 类用于发起订购披萨的订单。

会出现什么问题

以上这种传统的方式虽然实现简单,但是违背了开闭原则。假如现在想要新增一个披萨的种类,例如新增ChicagoPizza,则需要新创建一个 ChicagoPizza 类,再继承 Pizza 类,并且还需要在 OrderPizza 类的构造函数中添加else if语句。这种方法显然没有按照“扩展开放,修改关闭”的原则进行设计。

那么如何改进呢?

可以把创建 Pizza 对象封装到一个类中,当新增披萨种类的时候,只需要修改该类就可以,其它在创建 Pizza 对象的地方就不需要修改了。因此,这种方式也被称为简单工厂模式

简单工厂模式属于工厂模式的一种,它由一个工厂对象来决定创建出哪一种产品类的实例。也就是定义了一个创建对象的类,由这个类来封装实例化对象的行为。该模式在大量的创建某种、某类或某批对象的时候较为常用。

原先创建对象的行为属于 OrderPizza 类中,现在可以创建一个SimpleFactory类,将该类和 OrderPizza 类进行关联,如果想要订购某种类型的披萨,那么直接让SimpleFactory类去处理就可以了,然后返回给用户。

使用简单工厂模式进行改进,如下所示:

 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
// SimpleFactory.java
// 声明一个简单工厂
public class SimpleFactory {

    // 根据 orderType 返回对应的 Pizza 对象
    public Pizza createPizza(String orderType) {
        Pizza pizza = null;

        if (orderType.equals("greek")) {
            pizza = new GreekPizza();
            pizza.setName("GreekPizza");
        } else if (orderType.equals("cheese")) {
            pizza = new CheesePizza();
            pizza.setName("CheesePizza");
        }
        return pizza;
    }
}

// OrderPizza.java
public class OrderPizza {
    SimpleFactory simpleFactory;
    Pizza pizza = null;

    public OrderPizza (SimpleFactory simpleFactory) {
        setSimpleFactory(simpleFactory);
    }

    public void setSimpleFactory(SimpleFactory simpleFactory) {
        this.simpleFactory = simpleFactory;

        String orderType = "";
        do {
            orderType = getType();
            // 已经把创建对象的细节封装在了 createPizza 中
            pizza = simpleFactory.createPizza(orderType);

            // 披萨订购成功,输出相应信息
            if (pizza != null) {
                pizza.prepare();
                pizza.back();
                pizza.cut();
                pizza.box();
            } else {
                System.out.println("订购失败...");
                break;
            }
        } while (true);
    }


    private String getType() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入披萨类型:");
            String str = reader.readLine();
            return str;
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }
}

// PizzaStore.java
public class PizzaStore {
    public static void main(String[] args) {
        new OrderPizza(new SimpleFactory());
    }
}

输出结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
请输入披萨类型:
greek
给 GreekPizza 准备原材料...
GreekPizza backing...
GreekPizza cutting...
GreekPizza boxing...
请输入披萨类型:
12
订购失败...

Process finished with exit code 0

这里通过 SimpleFactory 实现了一个简单工厂,然后将创建对象的逻辑封装在 createPizza() 方法中。将来如果新增了一种类型的披萨,那么直接修改 SimpleFactory 即可,这样实现更加灵活。需要提到的是,如果将 createPizza() 方法声明为 static,即静态的,则将以上的方法称为静态简单工厂

但如果又有一个新的需求,即客户在点披萨的时候,可以点不同地区、不同口味的披萨。例如,北京的 GreekPizza、北京的 CheesePizza,上海的 GreekPizza、上海的 CheesePizza。

对于这种需求,也可以使用简单工厂模式,例如 BJPizzaSimpleFactory、SHPizzaSimpleFactory,但如果还有其它地区的披萨,则需要不断地进行创建。考虑到项目的规模,这种方式实现起来不易维护、不易扩展。

因此,可以使用工厂方法模式。即将披萨项目的实例化功能抽象成抽象方法,在不同的子类中进行实现。也就是说,工厂方法模式定义了一个创建对象的抽象方法,由子类决定要实例化的类,将对象的实例化推迟到子类中。

下面通过使用工厂方法模式来实现以上需求,在 OrderPizza 类中实现一个抽象的 createPizza() 方法,然后让 BJOrderPizza 和 SHOrderPizza 去继承 OrderPizza 类,然后在各自的子类中去实现不同地区口味的披萨。如下所示:

  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
 99
100
101
102
103
// OrderPizza.java
public abstract class OrderPizza {
    // 该抽象方法由各个工厂子类自己实现
    abstract Pizza createPizza(String orderType);

    public OrderPizza() {
        Pizza pizza = null;
        String orderType = "";
        do {
            orderType = getType();
            // 重要:根据不同的 orderType 实现不同类型或者口味的披萨
            pizza = createPizza(orderType);
            pizza.prepare();
            pizza.back();
            pizza.cut();
            pizza.box();

        } while (true);
    }

    private String getType() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入披萨类型:");
            String str = reader.readLine();
            return str;
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }
}

// BJOrderPizza.java
public class BJOrderPizza extends OrderPizza{
    @Override
    Pizza createPizza(String orderType) {
        Pizza pizza = null;
        if (orderType.equals("greek")) {
            pizza = new BJGreekPizza();
        } else if (orderType.equals("cheese")) {
            pizza = new BJCheesePizza();
        }
        return pizza;
    }
}

// SHOrderPizza.java
public class SHOrderPizza extends OrderPizza {
    @Override
    Pizza createPizza(String orderType) {
        Pizza pizza = null;
        if (orderType.equals("greek")) {
            pizza = new SHGreekPizza();
        } else if (orderType.equals("cheese")) {
            pizza = new SHCheesePizza();
        }
        return pizza;
    }
}

// BJCheesePizza.java
public class BJCheesePizza extends Pizza {
    @Override
    public void prepare() {
        setName("北京的 CheesePizza...");
        System.out.println("给 BJCheesePizza 准备原材料...");
    }
}

// BJGreekPizza.java
public class BJGreekPizza extends Pizza {
    @Override
    public void prepare() {
        setName("北京的 GreekPizza...");
        System.out.println("给 BJGreekPizza 准备原材料...");
    }
}

// SHCheesePizza.java
public class SHCheesePizza extends Pizza{
    @Override
    public void prepare() {
        setName("上海的 GreekPizza...");
        System.out.println("给 SHGreekPizza 准备原材料...");
    }
}

// SHGreekPizza.java
public class SHGreekPizza extends Pizza {
    @Override
    public void prepare() {
        setName("上海的 GreekPizza...");
        System.out.println("给 SHGreekPizza 准备原材料...");
    }
}

// PizzaStore.java
public class PizzaStore {
    public static void main(String[] args) {
        new BJOrderPizza();
    }
}

输出结果如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
请输入披萨类型:
greek
给 BJGreekPizza 准备原材料...
北京的 GreekPizza... backing...
北京的 GreekPizza... cutting...
北京的 GreekPizza... boxing...
请输入披萨类型:
cheese
给 BJCheesePizza 准备原材料...
北京的 CheesePizza... backing...
北京的 CheesePizza... cutting...
北京的 CheesePizza... boxing...

由于在主函数中创建的是北京地区的披萨,所以最终结果输出的都是与北京相关的。以上代码首先定义了一个抽象的 OrderPizza 类,里面有一个抽象的 createPizza() 方法,这个方法去交给子类 BJOrderPizza 和 SHOrderPizza 实现,它们返回的都是一个 Pizza 对象。

抽象工厂模式

工厂方法模式中的两个示例分别展示了简单工厂模式工厂方法模式,此外还有一种是抽象工厂模式,该模式可以将前两种进行整合。抽象工厂模式定义了一个接口,可以看做是对简单工厂模式的进一步封装。

下面使用抽象工厂模式来对订购披萨的需求进行实现,各类之间的关系如下所示:

 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
// AbsFactory.java
// 抽象模式的接口层
public interface AbsFactory {

    // 让工厂子类去具体实现
    Pizza createPizza(String orderType);
}

// BJFactory.java
public class BJFactory implements AbsFactory{
    @Override
    public Pizza createPizza(String orderType) {
        Pizza pizza = null;
        if (orderType.equals("greek")) {
            pizza = new BJGreekPizza();
        } else if (orderType.equals("cheese")) {
            pizza = new BJCheesePizza();
        }
        return pizza;
    }
}

// SHFactory.java
public class SHFactory implements AbsFactory {
    @Override
    public Pizza createPizza(String orderType) {
        Pizza pizza = null;
        if (orderType.equals("greek")) {
            pizza = new SHGreekPizza();
        } else if (orderType.equals("cheese")) {
            pizza = new SHCheesePizza();
        }
        return pizza;
    }
}

// OrderPizza.java
public class OrderPizza {

    AbsFactory factory;

    public OrderPizza (AbsFactory factory) {
        setFactory(factory);
    }

    private void setFactory(AbsFactory factory) {
        this.factory = factory;
        Pizza pizza = null;
        String orderType = "";

        do {
            orderType = getType();
            pizza = factory.createPizza(orderType);
            if (pizza != null) {
                pizza.prepare();
                pizza.back();
                pizza.cut();
                pizza.box();
            } else {
                System.out.println("订购失败...");
            }
        } while (true);
    }

    private String getType() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入披萨类型:");
            String str = reader.readLine();
            return str;
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }
}

// PizzaStore.java
public class PizzaStore {
    public static void main(String[] args) {
        new OrderPizza(new BJFactory());
    }
}

最后的结果与之前是一样的,这里就不再展开了。

在 JDK 中,Calendar 类使用到了简单工厂模式。从以上三个示例可以看出,抽象工厂模式的这种依赖抽象的原则是:在创建对象实例的时候,不直接使用 new 创建对象,而是把创建对象的动作放在一个工厂的方法中,然后再返回。并且,这里也不是让类继承具体的类,而是继承抽象类或实现接口。

建造者模式

建造者(Builder)模式创建的是复杂对象,将复杂对象的构造方式和它的表示方式进行分离,使同样的构建过程可以创建不同的表示。可以简单的将复杂对象理解为在创建对象 new XXX() 的时候,还需要设置许多属性,而相对应的简单对象就只需要在创建对象的时候通过 new XXX() 即可,不需要设置属性。

在创建复杂对象的时候,该对象通常由多个子部件按照一定的步骤组合而成,在没有组成之前,该对象不能作为一个完整的产品使用。例如电子邮件所包含的发件人、收件人、主题、内容等等。电子邮件由这么多的子部件组成,而最起码在收件人地址填写之前,该邮件是不能发送的,也即在产品没有组装完成之前是不能使用的。

因此,建造者模式可以将一个产品的内部表象与产品的生产过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。这里的内部表象是指:一个产品由不同的组成成分作为该产品的零件,我们把这些零件叫做产品的内部表象。不同的产品可以有不同的内部表象,也就是有不同的零件。使用建造者模式可以使客户端不需要知道所生产的产品有哪些零件,也不需要让客户端知道每个产品对应的零件彼此有何不同、怎么创建出来的、怎么组成的。

其实,它的核心思想就是:将产品的创建过程产品进行解耦。也就是说,如果把产品产品的创建过程封装在一起的话,就会增强它们之间的耦合性,不利于以后程序的扩展和维护。

下面通过使用传统的实现方式来完成建造房子的需求,我们可以将建造房子的过程分为打桩、砌墙、封顶。而可以创建的房子也有很多类型,如普通的房子、高楼大厦或者是别墅。这些房子的创建过程虽然一样,但是具体的要求是不同的。例如,对于打桩这一步骤来说,不同的房子所打桩的深度也是不同的。

 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
// AbstractHouse.java
public abstract class AbstractHouse {
    // 打桩
    public abstract void buildBasic();

    // 砌墙
    public abstract void buildWalls();

    // 封顶
    public abstract void roofed();

    // 盖房子
    public void build() {
        buildBasic();
        buildWalls();
        roofed();
    }
}

// 普通房子
public class CommonHouse extends AbstractHouse {

    @Override
    public void buildBasic() {
        System.out.println("普通房子打桩...");
    }

    @Override
    public void buildWalls() {
        System.out.println("普通房子砌墙...");
    }

    @Override
    public void roofed() {
        System.out.println("普通房子封顶...");
    }
}

// 高楼
public class HighHouse extends AbstractHouse {

    @Override
    public void buildBasic() {
        System.out.println("高楼打桩...");
    }

    @Override
    public void buildWalls() {
        System.out.println("高楼砌墙...");
    }

    @Override
    public void roofed() {
        System.out.println("高楼封顶...");
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        CommonHouse commonHouse = new CommonHouse();
        HighHouse highHouse = new HighHouse();

        commonHouse.build();
        highHouse.build();
    }
}

输出结果如下所示:

1
2
3
4
5
6
普通房子打桩...
普通房子砌墙...
普通房子封顶...
高楼打桩...
高楼砌墙...
高楼封顶...

对于以上的代码,首先定义了一个抽象类 AbstractHouse,该类是对房子的抽象,其中包括建造房子的步骤;然后定义了 HighHouse 和 CommonHouse 两个类,均继承了 AbstractHouse,并重写了里面的方法;最后定义了一个客户端 Client 来创建这两个房子。

这种传统的设计方式虽然便于理解,设计简单,但不利于程序的维护和扩展。例如,这种设计方式把产品(房子)和创建产品的过程(建造房子的流程)封装在了一起,增加了耦合性。因此,对于这种情况,可以通过建造者模式进行修改。

在修改之前,建造者模式还涉及到以下四种角色:

  • 产品(Product)角色:指一个具体的产品对象,也即建造中的复杂对象。一般来说,一个系统中会有一个以上的产品类,而这些产品类并不一定有共同的接口,完全可以是不相关联的。
  • 抽象建造者(Builder)角色:指创建一个 Product 对象的各个部件指定的接口(也可以是抽象类)。它主要用于给出一个抽象接口,用来规范产品对象的各个组成部分的建造。
  • 具体建造者(ConcreteBuilder)角色:该角色是与应用程序密切相关的一个类,它用于在应用程序的调用下创建产品的实例。也就是用于直接创建产品对象。该角色需要实现抽象建造者角色接口中的方法(如 buildPart1() 和 buildPart2() 方法),这些方法就是一步一步地完成创建产品实例的操作。并且在建造完成后,需要提供产品的实例(如 retrieveResult() 方法)。
  • 指挥者(Director)角色:构建一个使用 Builder 接口的对象以创建产品对象。该角色没有产品类的具体知识,真正拥有产品类的具体知识的是具体建造者角色。

下面用建造者模式来重新设计建造房子的需求:

  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
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 产品
public class House {
    private String basic;
    private String wall;
    private String roofed;

    public String getBasic() {
        return basic;
    }

    public void setBasic(String basic) {
        this.basic = basic;
    }

    public String getWall() {
        return wall;
    }

    public void setWall(String wall) {
        this.wall = wall;
    }

    public String getRoofed() {
        return roofed;
    }

    public void setRoofed(String roofed) {
        this.roofed = roofed;
    }
}

// 抽象的建造者
public abstract class HouseBuilder {

    House house = new House();

    // 打桩
    public abstract void buildBasic();

    // 砌墙
    public abstract void buildWalls();

    // 封顶
    public abstract void roofed();

    // 建造完房子以后,将产品(房子)返回
    public House buildHouse() {
        return house;
    }
}

// 普通房子
public class CommonHouse extends HouseBuilder {

    @Override
    public void buildBasic() {
        System.out.println("普通房子打桩 5 米...");
    }

    @Override
    public void buildWalls() {
        System.out.println("普通房子砌墙 2 米...");
    }

    @Override
    public void roofed() {
        System.out.println("普通房子封顶 1 米...");
    }
}

// 高楼
public class HighHouse extends HouseBuilder {

    @Override
    public void buildBasic() {
        System.out.println("高楼打桩 10 米...");
    }

    @Override
    public void buildWalls() {
        System.out.println("高楼砌墙 5 米...");
    }

    @Override
    public void roofed() {
        System.out.println("高楼封顶 10 米...");
    }
}

// 指挥者
public class HouseDirector {

    HouseBuilder houseBuilder = null;

    public HouseDirector(HouseBuilder houseBuilder) {
        this.houseBuilder = houseBuilder;
    }

    public void setHouseBuilder(HouseBuilder houseBuilder) {
        this.houseBuilder = houseBuilder;
    }

    // 如何建造房子的流程交给指挥者
    public House constructHouse() {
        houseBuilder.buildBasic();
        houseBuilder.buildWalls();
        houseBuilder.roofed();

        return houseBuilder.buildHouse();
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        // 现在想要建造普通的房子
        CommonHouse commonHouse = new CommonHouse();
        // 准备建造房子的指挥者
        HouseDirector houseDirector = new HouseDirector(commonHouse);
        // 完成盖房子,返回产品(房子)
        houseDirector.constructHouse();

        System.out.println("=================");

        HighHouse highHouse = new HighHouse();
        houseDirector.setHouseBuilder(highHouse);
        houseDirector.constructHouse();
    }
}

输出结果如下:

1
2
3
4
5
6
7
普通房子打桩 5 ...
普通房子砌墙 2 ...
普通房子封顶 1 ...
=================
高楼打桩 10 ...
高楼砌墙 5 ...
高楼封顶 10 ...

通过 UML 图可以看出以上类之间的关系,具体要建造高楼还是普通的房子,可以通过具体建造者角色来指定(如 CommonHouse、HighHouse),而抽象的建造者角色(HouseBuilder)包含了建造房子的抽象方法,以及用于返回产品的方法。而指挥者(HouseDirector)是与客户端(Client)打交道的的角色。指挥者将客户端创建产品的请求划分为对各个零件的建造请求,再将这些请求委派给具体建造者角色。最后,具体建造者角色是做具体建造工作的,但是却不为客户端所知。

当然,在 JDK 的源码中,StringBuilder 也使用到了建造者模式。如下所示:

1
2
3
4
5
6
public final class StringBuilder extends AbstractStringBuilder
                                implements java.io.Serializable, CharSequence {...}

abstract class AbstractStringBuilder implements Appendable, CharSequence {...}

public interface Appendable {...}

StringBuilder 继承了 AbstractStringBuilder,而 AbstractStringBuilder 实现了 Appendable 接口。在 Appendable 接口中定义的方法都是抽象的。因此,这里的 Appendable 充当了抽象建造者角色。而 AbstractStringBuilder 类已经把 Appendable 接口中的方法实现了,因此 AbstractStringBuilder 类在这里充当的是具体建造者角色,只不过不能实例化,因为它是抽象的。最后,StringBuilder 充当了具体建造者角色,同时也充当了指挥者角色,建造方法的实现是由 AbstractStringBuilder 完成的,而 StringBuilder 继承了 AbstractStringBuilder。

可以看到,虽然 StringBuilder 的源码结构与建造者模式有些出入,但其实也是满足建造者模式的。最后再总结一下建造者模式的细节:

  • 客户端(应用程序)不需要知道产品的内部组成细节,通过将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象;
  • 每一个具体创造者角色都是相对独立的,与其它具体创造者角色无关,用户使用不同的具体创造者角色可以得到不同的产品对象;
  • 可以更加精细的控制产品的创建过程;
  • 增加新的具体创造者角色无需修改原有类库的代码,而指挥者针对抽象建造者类进行编程。

原型模式

TODO

结构型模式(7 种)

代理模式

代理(proxy)模式的核心思想是:为某个对象提供一种代理以控制对该对象的访问,即客户端通过代理间接访问该对象,从而限制、增强或修改对象的一些特性。因此,这里所说的代理被称为代理对象,相当于房屋中介。引入代理对象后,客户端不能直接访问目标对象,代理对象作为客户端和目标对象之间的中介,以“经纪人”的身份帮你完成某些任务。

被代理的对象可以是远程对象、创建开销很大的对象或需要安全控制的对象。当访问的远程对象比较大时,在访问的时候可能会非常耗时,因此可以找一个中介来帮忙完成这个任务;对于需要安全控制的对象,因为安全原因客户端不能直接访问真实的对象,因此也可以通过中介来帮忙访问。

引入代理对象后,代理对象位于客户端和目标对象的中间,可以起到保护目标对象的作用。另外,代理对象还可以扩展目标对象的功能。最后,代理对象将客户端和目标对象进行了分离,在一定程度上降低了系统的耦合度。以上是代理模式的优点,当然也有缺点。例如由于代理对象处于客户端和目标对象的中间,因此请求处理的过程可能会很慢,此外,有可能增加系统的复杂度。

具体的代理方式有静态代理、动态代理(JDK 代理)、CGlib 代理,其中 CGlib 代理可以在内存中动态的创建对象,而不需要实现接口。

静态代理

静态代理在使用的时候需要注意的是:需要定义接口或父类,目标对象(被代理对象)与代理对象需要一起实现相同的接口或继承相同父类

下面通过教师上课的示例,对静态代理进行展开。如下所示:

 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
// 抽象对象角色,声明了目标对象和代理对象的共同接口
public interface ITeacherDAO {
    void teach();
}

/// 目标对象
public class TeacherDAO implements ITeacherDAO{

    @Override
    public void teach() {
        System.out.println("授课...");
    }
}

// 代理对象
public class TeacherDAOProxy implements ITeacherDAO {

    private ITeacherDAO target;

    public TeacherDAOProxy(ITeacherDAO target) {
        this.target = target;
    }

    @Override
    public void teach() {
        System.out.println("开始代理...");
        target.teach();
        System.out.println("代理结束...");
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        // 首先创建目标对象
        TeacherDAO teacherDAO = new TeacherDAO();

        // 然后创建代理对象,同时将目标对象传递给代理对象
        TeacherDAOProxy teacherDAOProxy = new TeacherDAOProxy(teacherDAO);

        // 通过代理对象,执行目标对象的方法
        // 即执行的是代理对象的方法,代理对象再去调用目标对象的方法
        teacherDAOProxy.teach();
    }
}

输出结果如下:

1
2
3
开始代理...
授课...
代理结束...

通过客户端Client.java的执行流程可以看到,客户端既然要访问目标对象,那么首先需要创建的就是目标对象 TeacherDAO,然后再创建一个代理对象 TeacherDAOProxy,同时将目标对象传递给代理对象,最后再执行代理对象的方法。需要注意的是,虽然执行的是代理对象中的方法,但是代理对象会再去调用目标对象的方法。

这样实现的好处是,在不修改目标对象中的功能的情况下,可以通过修改代理对象来对目标对象进行扩展。例如在 TeacherDAOProxy 类中的 teach 方法内部可以实现对目标对象中 teach 方法的扩充。但由于代理对象和目标对象都需要实现相同的接口,所以会有很多代理类。一旦接口增加了方法,那么目标对象和代理对象都需要维护,即也需要实现这些方法。

动态代理

在动态代理中,代理对象不需要实现接口,但是目标对象需要实现接口,否则不能使用动态代理。动态代理是通过 JDK 中的 java.lang.reflect.Proxy 中的 newProxyInstance 方法创建代理对象的,如下所示:

1
2
3
public static Object newProxyInstance(ClassLoader loader, 
                                    Class<?>[] interfaces, 
                                    InvocationHandler h) throws IllegalArgumentException {}

其中,loader指定当前目标对象使用的类加载器,获取加载器的方法是固定的;interfaces表示目标对象需要实现的接口类型,可以使用泛型的方法确认类型;h表示事件处理,当执行目标对象中方法的时候,会触发事件处理器方法,会把当前执行的目标对象方法作为参数传入。

下面通过示例展示动态代理的使用。

 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
// ITeacherDao.java
public interface ITeacherDao {
    void teach();
}

// 目标对象
public class TeacherDao implements ITeacherDao{

    @Override
    public void teach() {
        System.out.println("正在授课...");
    }
}

// 代理对象
public class ProxyFactory {

    // 维护目标对象
    private Object target;

    // 当创建对象的时候就对 target 进行初始化
    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 生成一个代理对象
    public Object getProxyInstance() {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("JDK 动态代理...");
                        // 利用反射机制调用目标对象的方法
                        Object invoke = method.invoke(target, args);
                        return invoke;
                    }
                });
    }
}

public class Client {
    public static void main(String[] args) {
        // 首先创建目标对象
        TeacherDao target = new TeacherDao();

        // 然后创建代理对象
        ITeacherDao proxyInstance = (ITeacherDao) new ProxyFactory(target).getProxyInstance();

        System.out.println(proxyInstance.getClass());

        // 通过代理对象调用目标对象的方法
        proxyInstance.teach();
    }
}

输出结果如下:

1
2
3
class com.sun.proxy.$Proxy0
JDK 动态代理...
正在授课...

这里用到了反射机制,最重要的就是 Proxy.newProxyInstance() 方法以及 invoke() 方法。当proxyInstance.teach();语句在执行的时候,回来到invoke()方法,进而通过反射的方式调用目标对象中的方法。

CGlib 代理

静态代理和动态代理(JDK 代理)都会让目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有任何接口可以实现,此时可以通过目标对象的子类来实现代理,即 CGlib 代理。

具体的实现方式是:在内存中构建一个子类对象,从而在运行期扩展 Java 类,以实现对目标对象功能上的扩展。例如 Spring AOP。但需要注意的是,被代理的类不能被 final 修饰,否则会抛出异常。目标对象中的方法如果被 final 或 static 修饰,则不会执行目标对象额外的业务方法,即不会有代理的效果。

Spring AOP 中如何实现代理模式?

  1. 目标对象需要实现接口,使用 JDK 代理;
  2. 目标对象不需要实现接口,使用 CGlib 代理。

在使用 CGlib 代理的时候,代理对象需要实现 MethodInterceptor 接口,并重写 intercept 方法,以便实现对目标对象方法的调用。如下所示:

 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
// 目标对象
public class TeacherDao {

    public void teach() {
        System.out.println("授课...CGlib 代理...");
    }
}

// ProxyFactory.java
public class ProxyFactory implements MethodInterceptor {

    // 维护一个目标对象
    private Object target;

    // 传入被代理的对象
    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 返回一个代理对象
    public Object getProxyInstance() {
        // 创建工具类
        Enhancer enhancer = new Enhancer();
        // 设置父类
        enhancer.setSuperclass(target.getClass());
        // 设置回调函数
        enhancer.setCallback(this);
        // 创建子类对象,即代理对象
        return enhancer.create();
    }

    // 重写 intercept 方法,将会调用目标对象中的方法
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGlib 代理开始...");
        Object res = method.invoke(target, args);
        System.out.println("CGlib 代理结束...");
        return res;
    }
}

// 客户端
public class Client {

    public static void main(String[] args) {
        // 创建目标对象
        TeacherDao target = new TeacherDao();

        // 获取代理对象,并将目标对象传递给代理对象
        TeacherDao proxyInstance = (TeacherDao) new ProxyFactory(target).getProxyInstance();

        // 执行代理对象的方法,触发 intercept 方法,从而实现对目标对象的调用
        proxyInstance.teach();
    }
}

输出结果如下:

1
2
3
CGlib 代理开始...
授课...CGlib 代理...
CGlib 代理结束...

proxyInstance.teach();方法执行的时候,会被intercept()方法拦截,进而执行输出语句,而method.invoke(target, args)中的 method 就是 TeacherDao 中的 teach() 方法。最后如果又返回值的话就返回,没有的话就返回 null。

这里需要注意的是:代理模式和接下来要讲到的适配器模式的区别,在代理模式下的静态代理,它的代理对象和目标对象都实现了相同的接口,而适配器模式中的对象适配,只是将目标对象委派到了适配器对象中,目标对象和适配器对象并没有实现相同的接口

适配器模式

适配器(Adapter)模式也很好理解,想象以下场景。在现实生活中,经常会出现两个对象因接口不兼容而不能在一起工作的案例,此时需要第三者进行适配。例如将笔记本的画面投影到投影仪上,笔记本的接口是 HDMI 的,而投影仪的接口是 VGA 的,此时需要一个从 HDMI 到 VGA 的转换器(适配器),使用该适配器对两者(HDMI 接口和 VGA 接口)进行适配。

而在软件设计上,如果需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,而重新开发这些组件的成本又很高,此时可以使用适配器模式可以很好的解决该问题。也就是说,将某个类的接口转换成所希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能够进行工作

适配器的角色有三个:

  • 目标接口:即当前系统所期待的接口;
  • 适配者类:即被访问或被适配的现存组件库中的组件接口;
  • 适配器类:即我们在上面提到的适配器。

它们之间的调用关系是:用户调用适配器转换出来的目标接口方法,适配器类再调用适配者类的相关接口方法。适配器模式又细分为类适配器模式、对象适配器模式以及接口适配器模式

类适配器模式

在类适配器模式下,首先会有一个适配器类(Adapter),它通过继承适配者类来实现目标类,目标量通过接口来使用。下面通过一个示例来说明类适配器的使用。该示例描述的是通过使用电源适配器给手机充电,由于插排所提供的电压是 220V 的,而手机只能接受 5V,因此需要第三者电源适配器来进行电压的转换。

 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
// 适配者类
public class Voltage220V {

    public int output220V() {
        int src = 220;
        System.out.println("电压 = " + src + "V...");
        return src;
    }
}

// IVoltage5V.java
public interface IVoltage5V {
    int output5V();
}

// 适配器类
public class VoltageAdapter extends Voltage220V implements IVoltage5V {
    @Override
    public int output5V() {
        // 拿到 220V 电压之后将其处理成 5V 电压,然后返回
        int src = output220V();
        int dst = src / 44;
        return dst;
    }
}

// Phone.java
public class Phone {

    public void charging(IVoltage5V iVoltage5V) {
        if (iVoltage5V.output5V() == 5) {
            System.out.println("电压为 5V,手机开始充电...");
        } else {
            System.out.println("电压不为 5V,不适合手机充电...");
        }
    }
}

// Client.java
public class Client {

    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.charging(new VoltageAdapter());
    }
}

输出结果如下:

1
2
电压 = 220V...
电压为 5V,手机开始充电...

在适配者类(Voltage220V)中,它提供的电压是 220V,我们需要通过适配器类(VoltageAdapter)将 220V 转换成手机能够充电的 5V,适配器类通过继承与实现接口的方式进行电压的转换。

需要注意的是,以上的这种类适配器模式有一定的局限性,因为 Java 只支持单继承。此外,适配者类中的output220V()方法也在适配器类(VoltageAdapter)中暴露了出来,增加了使用成本。但正因为继承了适配者类,所以它可以根据需求重写适配者类中的方法,使得适配器类的灵活性增强了。

对象适配器模式

与类适配器不同的是,对象适配器不是使用继承关系连接到适配者类,而是通过委派关系连接到适配者类,即持有适配者类的实例。对象适配器模式是适配器模式常用的一种设计模式。

实现的方式也很简单,直接修改适配器类即可,如下所示:

 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
// 适配器类
public class VoltageAdapter implements IVoltage5V {

    private Voltage220V voltage220V;

    public VoltageAdapter(Voltage220V voltage220V) {
        this.voltage220V = voltage220V;
    }

    @Override
    public int output5V() {
        int dst = 0;
        if (voltage220V != null) {
            int src = voltage220V.output220V();
            dst = src / 44;
        }
        return dst;
    }
}

// 客户端也进行了些许的改变
public class Client {

    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.charging(new VoltageAdapter(new Voltage220V()));
    }
}

接口适配器模式

当不需要全部实现接口提供的方法时,可以设计一个抽象类去实现该接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类就可以有选择性的覆盖父类的某些方法来实现需求。该适配器模式适用于一个接口不想使用其所有方法的情况。如下所示。

 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
// InterfaceDemo.java
public interface InterfaceDemo {

    // 只关注 fun1() 方法的使用
    void fun1();

    void fun2();

    void fun3();

    void fun4();
}

// AbsAdapter.java
public abstract class AbsAdapter implements InterfaceDemo{
    @Override
    public void fun1() {

    }

    @Override
    public void fun2() {

    }

    @Override
    public void fun3() {

    }

    @Override
    public void fun4() {

    }
}

// Client.java
public class Client {

    public static void main(String[] args) {

        AbsAdapter absAdapter = new AbsAdapter() {
            // 真正想要适配的是 fun1() 方法
            @Override
            public void fun1() {
                System.out.println("使用了 fun1 方法");
            }
        };
        absAdapter.fun1();
    }
}

输出结果如下:

1
使用了 fun1 方法

在 main 方法中我们只关注的是 fun1() 方法的使用,因此可以直接对 fun1() 方法进行重写即可。

小结

在 Spring MVC 的源码中,HandlerAdapter 使用到了适配器模式,为什么需要使用适配器模式?

这与 Spring MVC 的处理请求的流程有关,Handler 的类型不同,有多重实现方式,那么调用方式就是不确定的。如果需要直接调用 Controller 方法,那么在调用的时候就需要使用 if-else 的方式来判断是哪一个子类然后去执行,如果后面想要对 Controller 进行扩展,就需要修改原来的代码,不易维护。

桥接模式

TODO

装饰模式

装饰(Decorator)模式的核心是:在不改变现有对象结构的情况下,动态的给对象增加一些职责,即动态的将新的功能附加到对象上。该模式比继承更有弹性、更加灵活。通过设计不同的装饰类,可以创造出多种不同行为的组合。

为什么不用继承呢?因为当扩展一个类的功能的时候,如果通过继承来实现,那么它们之间的耦合度很高,并且随着扩展功能的增多,子类会很膨胀。因此,装饰模式是继承的一种替代方案。

装饰模式主要有以下角色:

  • 抽象构件(Component)角色:定义了一个抽象接口,用来规范准备接受附加责任的对象;
  • 具体构件(Concrete Component)角色:实现抽象接口,通过装饰角色为其添加一些职责;
  • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能;
  • 具体装饰(Concreate Decorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

假如要设计以下订购咖啡的业务,左侧的 ShortBlack、Decaf、Espresso、LongBlack 表示单品咖啡,即不添加任何调料的咖啡类型,它们所担任的角色就是被修饰者。还有一个抽象的 Drink,它含有一个对咖啡描述的方法 description(),以及计算价格的方法 cost()。由于单品咖啡的种类很多,因此又添加了一个 Coffee。

此外,还有一个 Decorator 表示装饰者,在它的右边有许多装饰者,例如你在买了单品咖啡后是否添加巧克力 Chocolate、是否添加牛奶 Milk、是否添加豆浆 Soy 等。

image.png

下面通过示例来展开对装饰模式的说明。

首先定义不同的单品咖啡:

 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
// ShortBlack.java
public class ShortBlack extends Coffee{

    public ShortBlack() {
        setDes("ShortBlack 咖啡...");
        setPrice(7.0F);
    }
}

// Decaf.java
public class Decaf extends Coffee {
    public Decaf() {
        setDes("Decaf 咖啡...");
        setPrice(10.0F);
    }
}

// Espresso.java
public class Espresso extends Coffee {

    public Espresso() {
        setDes("Espresso 咖啡...");
        setPrice(6.0F);
    }
}

// LongBlack.java
public class LongBlack extends Coffee {
    public LongBlack() {
        setDes("LongBlack 咖啡...");
        setPrice(11.0F);
    }
}

然后定义 Drink:

 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
public abstract class Drink {

    public String des;
    private float price = 0.0F;

    public String getDes() {
        return des;
    }

    public void setDes(String des) {
        this.des = des;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    // 计算费用的方法
    // 由具体的子类实现
    public abstract float cost();
}

再创建 Coffee:

1
2
3
4
5
6
public class Coffee extends Drink {
    @Override
    public float cost() {
        return super.getPrice();
    }
}

再创建 Decorator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Decorator extends Drink {
    private Drink drink;

    public Decorator(Drink drink) {
        this.drink = drink;
    }

    @Override
    public float cost() {
        // 调料的价格 + 单品咖啡的价格
        return super.getPrice() + drink.cost();
    }

    @Override
    public String getDes() {
        return super.des + " " + super.getPrice() + " && " + drink.getDes();
    }
}

再创建不同的调料:

 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
// Chocolate.java
// 描述当前调料以及调料的价格
public class Chocolate extends Decorator {
    public Chocolate(Drink drink) {
        super(drink);
        setDes("Chocolate...");
        setPrice(2.0F);
    }
}

// Milk.java
public class Milk extends Decorator {

    public Milk(Drink drink) {
        super(drink);
        setDes("Milk...");
        setPrice(1.2F);
    }
}

// Soy.java
public class Soy extends Decorator {

    public Soy(Drink drink) {
        super(drink);
        setDes("Soy...");
        setPrice(0.6F);
    }
}

最后,通过定义一个咖啡店,来订购一些加了调料的咖啡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 咖啡店
public class CofferStore {
    public static void main(String[] args) {
        // 点一份 LongBlack
        Drink drink = new LongBlack();
        System.out.println("未装饰前:");
        System.out.println(drink.cost());
        System.out.println(drink.getDes());
        System.out.println("============================");

        // 加入牛奶,相当于对 LongBlack 进行了装饰
        drink = new Milk(drink);
        System.out.println("装饰后:");
        System.out.println(drink.cost());
        System.out.println(drink.getDes());
        System.out.println("============================");

        // 再加入一份巧克力
        drink = new Chocolate(drink);
        System.out.println("装饰后:");
        System.out.println(drink.cost());
        System.out.println(drink.getDes());
    }
}

运行结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
未装饰前:
11.0
LongBlack 咖啡...
============================
装饰后:
12.2
Milk... 1.2 && LongBlack 咖啡...
============================
装饰后:
14.2
Chocolate... 2.0 && Milk... 1.2 && LongBlack 咖啡...

效果很明显,在没有对 Drink 进行装饰的时候,其输出的仅仅是单品咖啡的价格,而如果对单品咖啡进行了修饰,例如加入了牛奶或者巧克力,那么就会得到装饰后的效果。

Java 中的 I/O 流使用到了装饰模式,如下图所示:

image.png

如果看明白了示例,那么也就很容易明白上图的组成:

  • InputStream 相当于示例中的 Drink;
  • FileInputStream、StringBufferInputStream、ByteArrayInputStream 相当于单品咖啡;
  • 而 FilterInputStream 相当于 Decorator,即装饰着;
  • 而 BufferInputStream、DataInputStream、LineNumberInputStream 相当于调味。

在 FilterInputStream 类中有一个 InputStream,这就相当于 Decorator 中有一个 Drink 一样。因此,抽象类 InputStream 相当于抽象构件角色,FileInputStream、StringBufferInputStream、ByteArrayInputStream 相当于具体构件角色,FilterInputStream 相当于抽象装饰角色,而BufferInputStream、DataInputStream、LineNumberInputStream 相当于具体装饰角色

组合模式

TODO

外观模式

TODO

享元模式

TODO

行为型模式(11 种)

模板方法模式

责任链模式

策略模式

迭代器模式

观察者模式

状态模式

命令模式

TODO

中介者模式

TODO

备忘录模式

TODO

解释器模式

TODO

访问者模式

TODO

参考