├── .nojekyll
├── .gitignore
├── img
├── facade.png
├── adapter-class.png
├── adapter-example.png
├── adapter-structure.png
├── adapter-too-many.jpg
├── adapter-two-way.png
├── facade-as-interface.png
├── adapter-4pin-to-sata.png
├── factory-method-ac-ioc-di.png
├── factory-method-ac-normal.png
├── factory-method-ac-unlink.png
├── factory-method-framework.png
├── factory-method-structure.png
├── adapter-cannot-directly-use.png
├── factory-method-structure-2.png
├── factory-method-client-call-1.png
├── factory-method-client-call-2.png
├── factory-method-parallel-class.png
├── factory-method-framework-x-app-2.png
├── factory-method-framework-x-app.png
└── adapter-modify-impl-cause-problem.png
├── 附_UML.md
├── 7_生成器模式.md
├── code
└── typescript
│ ├── 15_TemplateMethod.ts
│ ├── 05_FactoryMethod.ts
│ ├── 03_Adapter.ts
│ ├── 10_Proxy.ts
│ ├── 02_Facade.ts
│ ├── 08_Prototype.ts
│ ├── 01_SimpleFactory.ts
│ ├── 17_State.ts
│ ├── 16_Strategy.ts
│ ├── 07_Builder.ts
│ ├── 22_Chain_Of_Responsibility.ts
│ ├── 04_Singleton.ts
│ ├── 18_Memento.ts
│ ├── 12_Command.ts
│ ├── 23_Bridge.ts
│ ├── 06_AbstractFactory.ts
│ ├── 11_Observer.ts
│ ├── 19_Flyweight.ts
│ ├── 09_Mediator.ts
│ ├── 14_Composite.ts
│ └── 13_Iterator.ts
├── _sidebar.md
├── index.html
├── 1_简单工厂.md
├── README.md
├── 4_单例模式.md
├── 3_适配器模式_2.md
├── 3_适配器模式.md
├── 2_外观模式.md
├── 5_工厂方法模式.md
├── 5_工厂方法模式_2.md
├── 6_抽象工厂模式_2.md
├── 6_抽象工厂模式.md
└── 4_单例模式_2.md
/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Thumbs.db
3 | .history
4 |
--------------------------------------------------------------------------------
/img/facade.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/facade.png
--------------------------------------------------------------------------------
/img/adapter-class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-class.png
--------------------------------------------------------------------------------
/img/adapter-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-example.png
--------------------------------------------------------------------------------
/img/adapter-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-structure.png
--------------------------------------------------------------------------------
/img/adapter-too-many.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-too-many.jpg
--------------------------------------------------------------------------------
/img/adapter-two-way.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-two-way.png
--------------------------------------------------------------------------------
/img/facade-as-interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/facade-as-interface.png
--------------------------------------------------------------------------------
/img/adapter-4pin-to-sata.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-4pin-to-sata.png
--------------------------------------------------------------------------------
/img/factory-method-ac-ioc-di.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-ac-ioc-di.png
--------------------------------------------------------------------------------
/img/factory-method-ac-normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-ac-normal.png
--------------------------------------------------------------------------------
/img/factory-method-ac-unlink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-ac-unlink.png
--------------------------------------------------------------------------------
/img/factory-method-framework.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-framework.png
--------------------------------------------------------------------------------
/img/factory-method-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-structure.png
--------------------------------------------------------------------------------
/img/adapter-cannot-directly-use.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-cannot-directly-use.png
--------------------------------------------------------------------------------
/img/factory-method-structure-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-structure-2.png
--------------------------------------------------------------------------------
/img/factory-method-client-call-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-client-call-1.png
--------------------------------------------------------------------------------
/img/factory-method-client-call-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-client-call-2.png
--------------------------------------------------------------------------------
/img/factory-method-parallel-class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-parallel-class.png
--------------------------------------------------------------------------------
/img/factory-method-framework-x-app-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-framework-x-app-2.png
--------------------------------------------------------------------------------
/img/factory-method-framework-x-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/factory-method-framework-x-app.png
--------------------------------------------------------------------------------
/img/adapter-modify-impl-cause-problem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qwqcode/DesignPatterns/HEAD/img/adapter-modify-impl-cause-problem.png
--------------------------------------------------------------------------------
/附_UML.md:
--------------------------------------------------------------------------------
1 | # UML
2 |
3 | ## 关系标识
4 |
5 | - 实线+没有箭头:代表存在关联
6 | - 实现+空心箭头:代表泛化 (类的继承,父子关系)
7 | - 虚线+普通箭头:代表依赖关系
8 | - 虚线+空心箭头:代表实现关系 (接口的实现)
9 | - 虚线+普通箭头:代表使用关系 (调用其功能)
10 | - 虚线+普通箭头:流 (一个对象在相继时间内的两种形式/状态)
11 |
--------------------------------------------------------------------------------
/7_生成器模式.md:
--------------------------------------------------------------------------------
1 | ## 生成器模式 (Builder)
2 |
3 | ## 场景问题
4 |
5 | ### 继续导出数据的应用框架
6 |
7 | 在讨论工厂方法模式的时候,提到了一个导出数据的应用框架。
8 |
9 | 对于导出数据的应用框架,通常在导出数据上,会有一些约定的方式,比如「导出文本格式」、「数据库备份形式」、「Excel 格式」、「XML 格式」等。
10 |
11 | 在「工厂方法」模式的章节里面,讨论并使用工厂方法模式来解决了「如何选择具体导出方式的问题」,并没有涉及到每种方式具体如何实现。
12 |
13 | 换句话说,在讨论工厂方法模式的时候,并「没有讨论如何实现导出成文本、XML 等具体的格式」,本章就来讨论这个问题。
14 |
15 | 对于「导出数据的应用框架」,通常对于具体的导出内容和格式是有要求的,假如现在有如下的要求,简单描述一下:
16 |
17 | - 导出文件,不管什么格式,都分成 3 个部分,分别是「文件头、文件体和文件尾」。
18 | - 在文件头部分,需要描述如下信息:分公司或门市点编号、导出数据的日期,对于文件格式,中间用逗号分隔。
19 | - 在文件体部分,
--------------------------------------------------------------------------------
/code/typescript/15_TemplateMethod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 模板方法模式 (Template Method)
3 | *
4 | * 「固定算法骨架」
5 | */
6 |
7 | /**
8 | * 定义模板方法、原语操作等的抽象类
9 | */
10 | abstract class AbstractClass {
11 | /**
12 | * 原语操作 1,所谓原语操作就是抽象的操作,必须要由子类提供实现
13 | */
14 | public abstract doPrimitiveOperation1(): void;
15 |
16 | /**
17 | * 原语操作 2
18 | */
19 | public abstract doPrimitiveOperation2(): void;
20 |
21 | /**
22 | * 模板方法,定义算法骨架
23 | */
24 | public templateMethod(): void {
25 | this.doPrimitiveOperation1();
26 | this.doPrimitiveOperation2();
27 | }
28 | }
29 |
30 | /**
31 | * 具体实现类,实现原语操作
32 | */
33 | class ConcreteClass extends AbstractClass {
34 | public doPrimitiveOperation1(): void {
35 | // 具体的实现
36 | }
37 |
38 | public doPrimitiveOperation2(): void {
39 | // 具体的实现
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/_sidebar.md:
--------------------------------------------------------------------------------
1 | * [学习设计模式](/)
2 | * [1. 简单工厂](./1_简单工厂.md)
3 | * [2. 外观模式 (Facade)](./2_外观模式.md)
4 | * [3. 适配器模式 (Adapter)](./3_适配器模式.md)
5 | * [4. 单例模式 (Singleton)](./4_单例模式.md)
6 | * [5. 工厂方法模式 (Factory Method)](./5_工厂方法模式.md)
7 | * [6. 抽象工厂模式 (Abstract Factory)](./6_抽象工厂模式.md)
8 | * [7. 生成器模式 (Builder)](./7_生成器模式.md)
9 | * [8. 原型模式 (Prototype)](./8_原型模式.md)
10 | * [9. 中介者模式 (Mediator)](./9_中介者模式.md)
11 | * [10. 代理模式 (Proxy)](./10_代理模式.md)
12 | * [11. 观察者模式 (Observer)](./11_观察者模式.md)
13 | * [12. 命令模式 (Command)](./12_命令模式.md)
14 | * [13. 迭代器模式 (Iterator)](./13_迭代器模式.md)
15 | * [14. 组合模式 (Composite)](./14_组合模式.md)
16 | * [15. 模版方法模式 (Template Method)](./15_模版方法模式.md)
17 | * [16. 策略模式 (Strategy)](./16_策略模式.md)
18 | * [17. 状态模式 (State)](./17_状态模式.md)
19 | * [18. 备忘录模式 (Memento)](./18_备忘录模式.md)
20 | * [19. 享元模式 (Flyweight)](./19_享元模式.md)
21 | * [20. 解释器模式 (Interpreter)](./20_解释器模式.md)
22 | * [21. 装饰模式 (Decorator)](./21_装饰模式.md)
23 | * [22. 职责链模式 (Chian of Responsibility)](./22_职责链模式.md)
24 | * [23. 桥接模式 (Bridge)](./23_桥接模式.md)
25 | * [24. 访问者模式 (Visitor)](./24_访问者模式.md)
26 | * [附: UML 图](./附_UML.md)
--------------------------------------------------------------------------------
/code/typescript/05_FactoryMethod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 工厂方法模式 (Factory Method)
3 | *
4 | * 「延迟到子类来选择实现」
5 | */
6 |
7 | /**
8 | * 工厂方法所创建的对象的接口
9 | */
10 | interface Product {
11 | // 可定义 Product 的属性和方法
12 | }
13 |
14 | /**
15 | * 具体的 Product 对象
16 | */
17 | class ConcreteProduct implements Product {
18 | // 实现 Product 要求的方法
19 | }
20 |
21 | /**
22 | * 创建器,声明工厂方法
23 | */
24 | abstract class Creator {
25 | /**
26 | * 创建 Product 的工厂方法
27 | * @return Product 对象
28 | */
29 | protected abstract factoryMethod(): Product;
30 |
31 | /**
32 | * 示意方法,实现某些功能的方法
33 | */
34 | public someOperation(): void {
35 | // 通常在这些方法中需要调用工厂方法来获取 Product 对象
36 | let product: Product = this.factoryMethod();
37 | }
38 | }
39 |
40 | /**
41 | * 具体的创建器实现对象
42 | */
43 | class ConcreteCreator extends Creator {
44 | protected factoryMethod(): Product {
45 | // 重定义工厂方法,返回一个具体的 Product 对象
46 | return new ConcreteProduct();
47 | }
48 | }
49 |
50 | /**
51 | * 客户端
52 | */
53 | ;(() => {
54 | let creator: Creator = new ConcreteCreator();
55 | creator.someOperation();
56 | })();
--------------------------------------------------------------------------------
/code/typescript/03_Adapter.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 适配器模式 (Adapter)
3 | *
4 | * 「转换匹配,复用功能」
5 | */
6 |
7 | /**
8 | * 定义客户端使用的接口,与特定领域相关
9 | */
10 | interface Target {
11 | /**
12 | * 示意方法,客户端请求处理的方法
13 | */
14 | request(): void;
15 | }
16 |
17 | /**
18 | * 已经存在的接口,这个接口需要被适配
19 | */
20 | class Adaptee {
21 | /**
22 | * 示意方法,原本已经存在,已经实现的方法
23 | */
24 | public specificRequest(): void {
25 | // 具体的功能处理
26 | }
27 | }
28 |
29 | /**
30 | * 适配器
31 | */
32 | class Adapter implements Target {
33 | /**
34 | * 持有需要被适配的接口对象
35 | */
36 | private adaptee: Adaptee;
37 |
38 | /**
39 | * 构造方法,传入需要被适配的对象
40 | * @param adaptee 需要被适配的对象
41 | */
42 | public constructor(adaptee: Adaptee) {
43 | this.adaptee = adaptee;
44 | }
45 |
46 | public request(): void {
47 | // 可能转调已经实现了的方法,进行适配
48 | this.adaptee.specificRequest();
49 | }
50 | }
51 |
52 | /**
53 | * 使用适配器的客户端
54 | */
55 | ;(() => {
56 | // 创建需要被适配的对象
57 | let adaptee: Adaptee = new Adaptee();
58 | // 创建客户端需要调用的接口对象
59 | let target: Target = new Adapter(adaptee);
60 | // 请求处理
61 | target.request();
62 | })();
--------------------------------------------------------------------------------
/code/typescript/10_Proxy.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 代理模式 (Proxy)
3 | *
4 | * 「控制对象访问」
5 | */
6 |
7 | /**
8 | * 抽象的目标接口,定义具体的目标对象和代理公用的接口
9 | */
10 | interface Subject {
11 | /**
12 | * 示意方法:一个抽象的请求方法
13 | */
14 | request(): void;
15 | }
16 |
17 | /**
18 | * 具体的目标对象,是真正被代理的对象
19 | */
20 | class RealSubject implements Subject {
21 | public request(): void {
22 | // 执行具体的功能处理
23 | }
24 | }
25 |
26 | /**
27 | * 代理对象
28 | */
29 | class MyProxy implements Subject {
30 | /**
31 | * 持有被代理的具体目标对象
32 | */
33 | private realSubject: RealSubject = null;
34 |
35 | /**
36 | * 构造方法,传入被代理的具体目标对象
37 | * @param realSubject 被代理的具体目标对象
38 | */
39 | public constructor(realSubject: RealSubject) {
40 | this.realSubject = realSubject;
41 | }
42 |
43 | public request(): void {
44 | // 在转调具体的目标对象前,可以执行一些功能处理
45 |
46 | // 转调具体的目标对象的方法
47 | this.realSubject.request();
48 |
49 | // 在转调具体的目标对象后,可以执行一些功能处理
50 | }
51 | }
52 |
53 | /**
54 | * 客户端
55 | */
56 | ;(() => {
57 | const realSubject = new RealSubject();
58 |
59 | const proxy = new MyProxy(realSubject);
60 | proxy.request();
61 | })();
62 |
--------------------------------------------------------------------------------
/code/typescript/02_Facade.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 外观模式 (Facade)
3 | *
4 | * 「封装交互,简化调用」
5 | */
6 |
7 | /**
8 | * A 模块的接口
9 | */
10 | interface AModuleApi {
11 | /**
12 | * 示意方法,A 模块对外的一个功能方法
13 | */
14 | testA(): void;
15 | }
16 |
17 | class AModuleImpl implements AModuleApi {
18 | public testA(): void {
19 | console.log("现在在 A 模块里操作 testA 方法");
20 | }
21 | }
22 |
23 | interface BModuleApi {
24 | testB(): void;
25 | }
26 |
27 | class BModuleImpl implements BModuleApi {
28 | public testB(): void {
29 | console.log("现在在 B 模块里面操作 testB 方法");
30 | }
31 | }
32 |
33 | interface CModuleApi {
34 | testC(): void;
35 | }
36 |
37 | class CModuleImpl implements CModuleApi {
38 | public testC(): void {
39 | console.log("现在在 C 模块里面操作 testC 方法");
40 | }
41 | }
42 |
43 | /**
44 | * 外观对象
45 | */
46 | class Facade {
47 | /**
48 | * 示意方法,满足客户端需要的功能
49 | */
50 | public test(): void {
51 | // 在内部实现的时候,可能会调用到内部的多个模块
52 | let a: AModuleApi = new AModuleImpl();
53 | a.testA();
54 | let b: BModuleApi = new BModuleImpl();
55 | b.testB();
56 | let c: CModuleApi = new CModuleImpl();
57 | c.testC();
58 | }
59 | }
60 |
61 | /**
62 | * 客户端
63 | */
64 | ;(() => {
65 | // 使用 Facade
66 | new Facade().test();
67 | })();
68 |
--------------------------------------------------------------------------------
/code/typescript/08_Prototype.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 原型模式 (Prototype)
3 | *
4 | * 「克隆生成对象」
5 | */
6 |
7 | /**
8 | * 声明一个克隆自身的接口
9 | */
10 | interface Prototype {
11 | /**
12 | * 克隆自身的方法
13 | * @return 一个从自身克隆出来的对象
14 | */
15 | clone(): Prototype;
16 | }
17 |
18 | /**
19 | * 克隆的具体实现对象
20 | */
21 | class ConcretePrototype1 implements Prototype {
22 | public clone(): Prototype {
23 | // 最简单的克隆,新建一个自生对象,由于没有属性,就不再复制了
24 | let prototype: ConcretePrototype1 = new ConcretePrototype1();
25 | return prototype;
26 | }
27 | }
28 |
29 | /**
30 | * 克隆的具体实现对象
31 | */
32 | class ConcretePrototype2 implements Prototype {
33 | private value1: string;
34 |
35 | public getValue1(): string {
36 | return this.value1;
37 | }
38 |
39 | public setValue1(val: string): void {
40 | this.value1 = val;
41 | }
42 |
43 | public clone(): Prototype {
44 | // 最简单的克隆,新建一个自身对象
45 | let prototype: ConcretePrototype2 = new ConcretePrototype2();
46 | // 复制属性
47 | prototype.setValue1(this.value1);
48 | return prototype;
49 | }
50 | }
51 |
52 | let prototype1 = new ConcretePrototype1()
53 | let prototype2 = new ConcretePrototype2()
54 |
55 | /**
56 | * 客户端
57 | *
58 | * @param prototype 需要克隆的原型
59 | */
60 | ;((prototype: Prototype) => {
61 | // 克隆原型
62 | let newPrototype: Prototype = prototype.clone();
63 | })(prototype2);
64 |
--------------------------------------------------------------------------------
/code/typescript/01_SimpleFactory.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 简单工厂模式
3 | *
4 | * 「选择实现」
5 | */
6 |
7 | /**
8 | * 接口的定义,该接口可以通过简单工厂来创建
9 | */
10 | interface Api {
11 | /**
12 | * 示意,具体功能方法的定义
13 | * @param s 示意,需要的参数
14 | */
15 | operation(s: string): void;
16 | }
17 |
18 | /**
19 | * 接口的具体实现对象 A
20 | */
21 | class ImplA implements Api {
22 | public operation(s: string): void {
23 | // 实现功能的代码,示意一下
24 | console.log("ImplA s=="+s);
25 | }
26 | }
27 |
28 | /**
29 | * 接口的具体实现对象 B
30 | */
31 | class ImplB implements Api {
32 | public operation(s: string): void {
33 | // 实现功能的代码,示意一下
34 | console.log("ImplB s=="+s);
35 | }
36 | }
37 |
38 | /**
39 | * 工厂类,用来创建 Api 对象
40 | */
41 | class Factory {
42 | /**
43 | * 具体创建 Api 对象的方法
44 | * @param condition 示意,从外部传入的选择条件
45 | */
46 | public static createApi(condition: number): Api {
47 | // 应该根据某些条件去选择究竟创建哪一个具体的实现对象,
48 | // 这些条件可以从外部传入,也可以从其他的途径来获取。
49 | // 如果只有一个实现,可以省略条件,因为没有选择的必要。
50 | // 示意使用条件
51 | let api: Api = null;
52 | if (condition == 1) {
53 | api = new ImplA();
54 | } else if (condition == 2) {
55 | api = new ImplB();
56 | }
57 |
58 | return api;
59 | }
60 | }
61 |
62 | /**
63 | * 客户端,使用 Api 接口
64 | */
65 | ;(() => {
66 | // 通过简单工厂来获取接口对象
67 | let api: Api = Factory.createApi(1);
68 | api.operation("正在使用简单工厂");
69 | })();
--------------------------------------------------------------------------------
/code/typescript/17_State.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 状态模式 (State)
3 | */
4 | interface State {
5 | /**
6 | * 状态对应的处理
7 | * @param sampleParameter 示例参数,说明可以传入参数,
8 | * 具体传入什么样的参数,传入几个参数,由具体的应用来具体分析
9 | */
10 | handle(sampleParameter: string): void;
11 | }
12 |
13 | /**
14 | * 实现一个与 Context 的一个特定状态相关的行为
15 | */
16 | class ConcreteStateA implements State {
17 | public handle(sampleParameter: string): void {
18 | // 实现具体的处理
19 | }
20 | }
21 |
22 | /**
23 | * 实现一个与 Context 的一个特定状态相关的行为
24 | */
25 | class ConcreteStateB implements State {
26 | public handle(sampleParameter: string): void {
27 | // 实现具体的处理
28 | }
29 | }
30 |
31 | /**
32 | * 定义客户感兴趣的接口,通常会维护一个 State 类型的对象实例
33 | */
34 | class Context {
35 | /**
36 | * 持有一个 State 类型的对象实例
37 | */
38 | private state: State;
39 |
40 | /**
41 | * 设置实现 State 的对象的实例
42 | * @param state 实现 State 的对象的实例
43 | */
44 | public setState(state: State) {
45 | this.state = state;
46 | }
47 |
48 | /**
49 | * 用户感兴趣的接口方法
50 | * @param sampleParameter 示意参数
51 | */
52 | public request(sampleParameter: string) {
53 | // 在处理中,会转调 state 来处理
54 | this.state.handle(sampleParameter);
55 | }
56 | }
57 |
58 | /**
59 | * 客户端
60 | */
61 | ;(() => {
62 | const context = new Context();
63 | const stateA: State = new ConcreteStateA();
64 | context.setState(stateA);
65 |
66 | context.request("param");
67 | })();
68 |
--------------------------------------------------------------------------------
/code/typescript/16_Strategy.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 策略模式 (Strategy)
3 | */
4 | interface Strategy {
5 | /**
6 | * 某个算法的接口,可以有传入参数,也可以有返回值
7 | */
8 | algorithmInterface(): void;
9 | }
10 |
11 | /**
12 | * 实现具体的算法 A
13 | */
14 | class ConcreteStrategyA implements Strategy {
15 | public algorithmInterface(): void {
16 | // 具体的算法实现
17 | }
18 | }
19 |
20 | /**
21 | * 实现具体的算法 B
22 | */
23 | class ConcreteStrategyB implements Strategy {
24 | public algorithmInterface(): void {
25 | // 具体的算法实现
26 | }
27 | }
28 |
29 | /**
30 | * 实现具体的算法 C
31 | */
32 | class ConcreteStrategyC implements Strategy {
33 | public algorithmInterface(): void {
34 | // 具体的算法实现
35 | }
36 | }
37 |
38 | /**
39 | * 上下文对象,通常会持有一个具体的策略对象
40 | */
41 | class Context {
42 | /**
43 | * 持有一个具体的策略对象
44 | */
45 | private strategy: Strategy;
46 |
47 | /**
48 | * 构造方法,传入一个具体的策略对象
49 | */
50 | public constructor(aStrategy: Strategy) {
51 | this.strategy = aStrategy;
52 | }
53 |
54 | /**
55 | * 上下文对客户端提供的操作接口,可以有参数和返回值
56 | */
57 | public contextInterface(): void {
58 | // 通常会转调具体的策略对象进行算法运算
59 | this.strategy.algorithmInterface();
60 | }
61 | }
62 |
63 | /**
64 | * 客户端
65 | */
66 | ;(() => {
67 | // 1. 选择并创建需要使用的策略对象
68 | const strategy: Strategy = new ConcreteStrategyA();
69 |
70 | // 2. 创建上下文
71 | const context: Context = new Context(strategy);
72 |
73 | // 3. 执行操作
74 | context.contextInterface();
75 | })();
76 |
--------------------------------------------------------------------------------
/code/typescript/07_Builder.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 生成器模式
3 | *
4 | * 「分离整体构建算法和部件构造」
5 | */
6 |
7 | /**
8 | * 生成器接口,定义创建一个产品对象所需的各个部件的操作
9 | */
10 | interface Builder {
11 | /**
12 | * 示意方法,构建某个部件
13 | */
14 | buildPart(): void;
15 | }
16 |
17 | /**
18 | * 被构建的产品对象的借口
19 | */
20 | interface Product {
21 | // 定义产品的操作
22 | }
23 |
24 | /**
25 | * 具体的生成器实现对象
26 | */
27 | class ConcreteBuilder implements Builder {
28 | /**
29 | * 生成器最终构建的产品对象
30 | */
31 | private resultProduct: Product;
32 |
33 | /**
34 | * 获取生成器最终构建的产品对象
35 | * @returns 生成器最终构建的产品对象
36 | */
37 | public getResult(): Product {
38 | return this.resultProduct;
39 | }
40 |
41 | public buildPart(): void {
42 | // 构建某个部件的功能处理
43 | }
44 | }
45 |
46 | /**
47 | * 指导者,指导使用生成器的接口来构建输出的文件的对象
48 | */
49 | class Director {
50 | /**
51 | * 持有当前需要使用的生成器对象
52 | */
53 | private builder: Builder;
54 |
55 | /**
56 | * 构造方法,传入生成器对象
57 | * @param builder 生成器对象
58 | */
59 | public constructor(builder: Builder) {
60 | this.builder = builder;
61 | }
62 |
63 | /**
64 | * 示意方法,指导生成器构建最终的产品对象
65 | */
66 | public construct(): void {
67 | // 通过使用生成器接口来构建最终的产品对象
68 | this.builder.buildPart();
69 | }
70 | }
71 |
72 | /**
73 | * 客户端
74 | */
75 | ;(() => {
76 | let concreteBuilder = new ConcreteBuilder();
77 | let director = new Director(concreteBuilder);
78 | director.construct();
79 | console.log(concreteBuilder.getResult());
80 | })();
81 |
--------------------------------------------------------------------------------
/code/typescript/22_Chain_Of_Responsibility.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 职责链模式 (Chain of Responsibility)
3 | */
4 |
5 | /**
6 | * 职责的接口,也就是处理请求的接口
7 | */
8 | abstract class Handler {
9 | /**
10 | * 持有后继的职责对象
11 | */
12 | protected successor: Handler;
13 |
14 | /**
15 | * 设置后继的职责对象
16 | * @param successor 后继的职责对象
17 | */
18 | public setSuccessor(successor: Handler) {
19 | this.successor = successor;
20 | }
21 |
22 | /**
23 | * 示意处理请求的方法,虽然这个示意方法是没有传入参数的
24 | * 但实际是可以传入参数的,根据具体需求来选择是否传入参数
25 | */
26 | public abstract handleRequest(): void;
27 | }
28 |
29 | /**
30 | * 具体的职责对象,用来处理请求
31 | */
32 | class ConcreteHandler1 extends Handler {
33 | public handleRequest(): void {
34 | // 根据某些条件来判断是否属于自己处理的职责范围
35 | // 判断条件比如,从外部传入的参数,或者这里主动去获取的外部数据,
36 | // 如从数据库中获取等,下面这句话是个示意
37 | const someCondition: boolean = false;
38 |
39 | if (someCondition) {
40 | // 如果属于自己处理的职责范围,就在这里处理请求
41 | // 具体的处理代码
42 | console.log("ConcreteHandler1 handle request");
43 | } else {
44 | // 如果不属于自己处理的职责范围,那就判断是否还有后继的职责对象
45 | // 如果有,就转发请求给后继的职责对象
46 | // 如果没有,什么都不做,自然结束
47 | if (this.successor !== null) {
48 | this.successor.handleRequest();
49 | }
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * 职责链的客户端
56 | */
57 | ;(() => {
58 | // 先要组装职责链
59 | const h1: Handler = new ConcreteHandler1();
60 | const h2: Handler = new ConcreteHandler2();
61 |
62 | h1.setSuccessor(h2);
63 | // 然后提交请求
64 | h1.handleRequest();
65 | })();
66 |
--------------------------------------------------------------------------------
/code/typescript/04_Singleton.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 单例模式 (Singleton)
3 | *
4 | * 「控制实例的数目」
5 | */
6 |
7 | /**
8 | * 懒汉式单例模式的示例
9 | */
10 | class SingletonLazy {
11 | /**
12 | * 定义一个变量来存储创建好的类实例
13 | */
14 | private static uniqueInstance: SingletonLazy = null;
15 |
16 | /**
17 | * 私有化构造方法,可以在内部控制创建实例的数目
18 | */
19 | private constructor() {
20 | //...
21 | }
22 |
23 | /**
24 | * 定义一个方法来为客户端提供类实例
25 | * @return 一个 Singleton 的实例
26 | */
27 | public static getInstance(): SingletonLazy {
28 | // 判断存储实例的变量是否有值
29 | if (this.uniqueInstance == null) {
30 | // 如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
31 | this.uniqueInstance = new SingletonLazy();
32 | }
33 |
34 | // 如果有值,那就直接使用
35 | return this.uniqueInstance;
36 | }
37 |
38 | /**
39 | * 示意方法,单例可以有自己的操作
40 | */
41 | public singletonOperation(): void {
42 | // 功能处理
43 | }
44 |
45 | /**
46 | * 示意属性,单利可以有自己的属性
47 | */
48 | private singletonData: string;
49 |
50 | /**
51 | * 示意方法,让外部通过这些方法来访问属性的值
52 | * @return 属性的值
53 | */
54 | public getSingletonData(): string {
55 | return this.singletonData;
56 | }
57 | }
58 |
59 | /**
60 | * 饿汉式单例模式的示例
61 | */
62 | class SingletonHungry {
63 | private static uniqueInstance: SingletonHungry = new SingletonHungry(); // 区别在于实例化的时期
64 |
65 | private constructor() {
66 | // ...
67 | }
68 |
69 | public static getInstance(): SingletonHungry {
70 | return this.uniqueInstance;
71 | }
72 |
73 | public singletonOperation(): void {
74 | // ...
75 | }
76 |
77 | private singletonData: string;
78 |
79 | public getSingletonData(): string {
80 | return this.singletonData;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/code/typescript/18_Memento.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 备忘录模式 (Memento)
3 | */
4 |
5 | /**
6 | * 备忘录的窄接口,没有任何方法定义
7 | */
8 | interface Memento {
9 | //
10 | }
11 |
12 | /**
13 | * 原发器对象
14 | */
15 | class Originator {
16 | /**
17 | * 示意,表示原发器的状态
18 | */
19 | private state: string = "";
20 |
21 | /**
22 | * 创建保存原发器对象的状态的备忘录对象
23 | * @return 创建好的备忘录对象
24 | */
25 | public createMemento(): Memento {
26 | return new MementoImpl(this.state);
27 | }
28 |
29 | /**
30 | * 重新设置原发器对象的状态,让其回到备忘录对象记录的状态
31 | * @param memento 记录有原发器状态的备忘录对象
32 | */
33 | public setMemento(memento: Memento) {
34 | const mementoImpl: MementoImpl = memento as MementoImpl;
35 | this.state = mementoImpl.getState();
36 | }
37 | }
38 |
39 | /**
40 | * 真正的备忘录对象,实现备忘录窄接口
41 | * 实现成私有的内部类,不让外部访问
42 | */
43 | class MementoImpl implements Memento {
44 | /**
45 | * 示意,表示需要保存的状态
46 | */
47 | private state: string = "";
48 |
49 | public constructor(state: string) {
50 | this.state = state;
51 | }
52 |
53 | public getState(): string {
54 | return this.state;
55 | // ↑ 创建过后,一般只让外面来访问数据,
56 | // 而不再修改数据,因此只有 getter
57 | }
58 | }
59 |
60 | /**
61 | * 负责保存备忘录的对象
62 | */
63 | class Caretaker {
64 | /**
65 | * 记录被保存的备忘录对象
66 | */
67 | private memento: Memento = null;
68 |
69 | /**
70 | * 保存备忘录对象
71 | * @param memento 被保存的备忘录对象
72 | */
73 | public saveMemento(memento: Memento): void {
74 | this.memento = memento;
75 | }
76 |
77 | /**
78 | * 获取被保存的备忘录对象
79 | * @return 被保存的备忘录对象
80 | */
81 | public retrieveMemento() {
82 | return this.memento;
83 | }
84 | }
85 |
86 | /**
87 | * 客户端
88 | */
89 | ;(() => {
90 | //...待补充
91 | })();
92 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 设计模式 @qwqcode's Notes
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/code/typescript/12_Command.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 命令模式 (Command)
3 | *
4 | * 「封装请求」
5 | */
6 |
7 | /**
8 | * 命令接口,声明执行的操作
9 | */
10 | interface Command {
11 | /**
12 | * 执行命令对应的操作
13 | */
14 | execute(): void;
15 | }
16 |
17 | /**
18 | * 具体的命令实现对象
19 | */
20 | class ConcreteCommand implements Command {
21 | /**
22 | * 持有相应的接收者对象
23 | */
24 | private receiver: Receiver = null
25 |
26 | /**
27 | * 示意,命令对象可以有自己的状态
28 | */
29 | private state: string;
30 |
31 | /**
32 | * 构造方法,传入相应的接收者对象
33 | * @param receiver 相应的接收者对象
34 | */
35 | public constructor(receiver: Receiver) {
36 | this.receiver = receiver;
37 | }
38 |
39 | public execute(): void {
40 | // 通常会转调接收者对象的相应方法,让接收者来真正执行功能
41 | this.receiver.action();
42 | }
43 | }
44 |
45 | /**
46 | * 接收者对象
47 | */
48 | class Receiver {
49 | /**
50 | * 示意方法,真正执行命令相应的操作
51 | */
52 | public action(): void {
53 | // 真正执行命令操作的功能代码
54 | }
55 | }
56 |
57 | /**
58 | * 调用者
59 | */
60 | class Invoker {
61 | /**
62 | * 持有命令对象
63 | */
64 | private command: Command = null;
65 |
66 | /**
67 | * 设置调用者持有的命令对象
68 | * @param command 命令对象
69 | */
70 | public setCommand(command: Command): void {
71 | this.command = command;
72 | }
73 |
74 | /**
75 | * 示意方法,要求命令执行请求
76 | */
77 | public runCommand(): void {
78 | // 调用命令对象的执行方法
79 | this.command.execute();
80 | }
81 | }
82 |
83 | /**
84 | * 客户端
85 | */
86 | ;(() => {
87 | // 创建接收者
88 | const receiver = new Receiver();
89 |
90 | // 创建命令对象,设定它的接收者
91 | const command = new ConcreteCommand(receiver);
92 |
93 | // 创建 Invoker,把命令对象设置进去
94 | const invoker = new Invoker();
95 | invoker.setCommand(command);
96 |
97 | invoker.runCommand();
98 | })();
99 |
--------------------------------------------------------------------------------
/code/typescript/23_Bridge.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 桥接模式 (Bridge)
3 | */
4 |
5 | /**
6 | * 定义实现部分的接口,可以与抽象部分的方法不一样
7 | */
8 | interface Implementor {
9 | /**
10 | * 示例方法,实现抽象部分需要的某些具体功能
11 | */
12 | operationImpl(): void;
13 | }
14 |
15 | /**
16 | * 定义抽象部分的接口
17 | */
18 | abstract class Abstraction {
19 | /**
20 | * 持有一个实现部分的接口
21 | */
22 | protected impl: Implementor;
23 |
24 | /**
25 | * 构造方法,传入实现部分的对象
26 | * @param impl 实现部分的对象
27 | */
28 | public constructor(impl: Implementor) {
29 | this.impl = impl;
30 | }
31 |
32 | /**
33 | * 示例操作,实现一定的功能,可能需要转调实现部分的具体实现方法
34 | */
35 | public operation(): void {
36 | this.impl.operationImpl();
37 | }
38 | }
39 |
40 | /**
41 | * 真正的具体实现对象
42 | */
43 | class ConcreteImplementorA implements Implementor {
44 | public operationImpl(): void {
45 | // 真正的实现
46 | }
47 | }
48 |
49 | /**
50 | * 另一个具体的实现对象
51 | */
52 | class ConcreteImplementorB implements Implementor {
53 | public operationImpl(): void {
54 | // 真正的实现
55 | }
56 | }
57 |
58 | /**
59 | * 扩充由 Abstraction 定义的接口对象
60 | */
61 | class RefinedAbstraction extends Abstraction {
62 | public constructor(impl: Implementor) {
63 | super(impl);
64 | }
65 |
66 | /**
67 | * 示例操作,实现一定的功能
68 | */
69 | public otherOperation(): void {
70 | // 实现一定的功能,可能会使用具体实现部分的实现方法
71 | // 但是本方法更大的可能是使用 Abstraction 中定义的方法
72 | // 通过组合使用 Abstraction 中定义的方法来完成更多的功能
73 | }
74 | }
75 |
76 | /**
77 | * 客户端
78 | */
79 | ;(() => {
80 | // 创建具体的实现对象
81 | const implA: Implementor = new ConcreteImplementorA();
82 | const implB: Implementor = new ConcreteImplementorB();
83 |
84 | const refinedA = new RefinedAbstraction(implA);
85 | refinedA.operation();
86 | refinedA.otherOperation();
87 |
88 | const refinedB = new RefinedAbstraction(implB);
89 | refinedB.operation();
90 | refinedB.otherOperation();
91 | });
92 |
--------------------------------------------------------------------------------
/code/typescript/06_AbstractFactory.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 抽象工厂模式 (Abstract Factory)
3 | *
4 | * 「选择产品族的实现」
5 | */
6 |
7 | /**
8 | * 抽象工厂的接口,声明创建抽象产品的操作
9 | */
10 | interface AbstractFactory {
11 | /**
12 | * 示例方法,创建抽象产品 A 的对象
13 | * @return 抽象产品 A 的对象
14 | */
15 | createProductA(): AbstractProductA;
16 |
17 | /**
18 | * 示例方法,创建抽象产品 B 的对象
19 | * @return 抽象产品 B 的对象
20 | */
21 | createProductB(): AbstractProductB;
22 | }
23 |
24 | /**
25 | * 抽象产品 A 的接口
26 | */
27 | interface AbstractProductA {
28 | // 定义抽象产品 A 相关的操作
29 | }
30 |
31 | /**
32 | * 抽象产品 B 的接口
33 | */
34 | interface AbstractProductB {
35 | // 定义抽象产品 B 相关的操作
36 | }
37 |
38 | /**
39 | * 产品 A 的具体实现
40 | */
41 | class ProductA1 implements AbstractProductA {
42 | // 实现产品 A 的接口中定义的操作
43 | }
44 |
45 | /**
46 | * 产品 A 的具体实现
47 | */
48 | class ProductA2 implements AbstractProductA {
49 | // 实现产品 A 的接口中定义的操作
50 | }
51 |
52 | /**
53 | * 产品 B 的具体实现
54 | */
55 | class ProductB1 implements AbstractProductB {
56 | // 实现产品 B 的接口中定义的操作
57 | }
58 |
59 | /**
60 | * 产品 B 的具体实现
61 | */
62 | class ProductB2 implements AbstractProductB {
63 | // 实现产品 B 的接口中定义的操作
64 | }
65 |
66 | /**
67 | * 具体的工厂实现对象,实现创建具体产品对象的操作
68 | */
69 | class ConcreteFactory1 implements AbstractFactory {
70 | public createProductA(): AbstractProductA {
71 | return new ProductA1();
72 | }
73 |
74 | public createProductB(): AbstractProductB {
75 | return new ProductB1();
76 | }
77 | }
78 |
79 | /**
80 | * 具体的工厂实现对象,实现创建具体的产品对象的操作
81 | */
82 | class ConcreteFactory2 implements AbstractFactory {
83 | public createProductA(): AbstractProductA {
84 | return new ProductA2();
85 | }
86 |
87 | public createProductB(): AbstractProductB {
88 | return new ProductB2();
89 | }
90 | }
91 |
92 | /**
93 | * 客户端
94 | */
95 | ;(() => {
96 | // 创建抽象工厂对象
97 | let af: AbstractFactory = new ConcreteFactory1();
98 |
99 | // 通过抽象工厂来获取一系列对象,如产品 A 和产品 B
100 | af.createProductA();
101 | af.createProductB();
102 | })();
--------------------------------------------------------------------------------
/code/typescript/11_Observer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 观察者模式 (Observer)
3 | *
4 | * 「触发联动」
5 | */
6 |
7 | /**
8 | * 目标对象,它知道它的观察者,并提供注册和删除观察者的接口
9 | */
10 | class Subject {
11 | /**
12 | * 用来保存注册观察者对象
13 | */
14 | private observers: Observer[] = [];
15 |
16 | /**
17 | * 注册观察者对象
18 | * @param observer 观察者对象
19 | */
20 | public attach(observer: Observer) {
21 | this.observers.push(observer);
22 | }
23 |
24 | /**
25 | * 删除观察者对象
26 | * @param observer 观察者对象
27 | */
28 | public detach(observer: Observer) {
29 | this.observers.splice(this.observers.indexOf(observer), 1);
30 | }
31 |
32 | /**
33 | * 通知所有注册的观察者对象
34 | */
35 | protected notifyObservers(): void {
36 | this.observers.forEach((observer) => {
37 | observer.update(this);
38 | });
39 | }
40 | }
41 |
42 | /**
43 | * 具体的目标对象,负责把有关状态存入到相应的观察者对象
44 | * 并把自己状态发生改变时,通知各个观察者
45 | */
46 | class ConcreteSubject extends Subject {
47 | /**
48 | * 示意,目标对象的状态
49 | */
50 | private subjectState: string;
51 |
52 | public getSubjectState(): string {
53 | return this.subjectState;
54 | }
55 |
56 | public setSubjectState(subjectState: string) {
57 | this.subjectState = subjectState;
58 | // 状态发生了改变,通知各个观察者
59 | this.notifyObservers();
60 | }
61 | }
62 |
63 | /**
64 | * 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象
65 | */
66 | interface Observer {
67 | /**
68 | * 更新的接口
69 | * @param subject 传入目标对象,方便获取相应的目标对象的状态
70 | */
71 | update(subject: Subject): void;
72 | }
73 |
74 | /**
75 | * 具体观察者对象,实现更新的方法,使自身的状态和目标状态保持一致
76 | */
77 | class ConcreteObserverA implements Observer {
78 | /**
79 | * 示意,观察者的状态
80 | */
81 | private observerState: string;
82 |
83 | public update(subject: Subject): void {
84 | // 具体的更新实现
85 | // 这里可能需要更新观察者的状态,使其与目标的状态保持一致
86 | this.observerState = (subject as ConcreteSubject).getSubjectState();
87 | }
88 | }
89 |
90 | class ConcreteObserverB implements Observer {
91 | private observerState: string;
92 |
93 | public update(subject: Subject): void {
94 | this.observerState = (subject as ConcreteSubject).getSubjectState();
95 | }
96 | }
97 |
98 | /**
99 | * 客户端
100 | */
101 | ;(() => {
102 | // 创建被观察者
103 | const subject = new ConcreteSubject();
104 |
105 | // 创建观察者
106 | const observerA = new ConcreteObserverA();
107 | const observerB = new ConcreteObserverB();
108 |
109 | // 注册观察者
110 | subject.attach(observerA);
111 | subject.attach(observerB);
112 |
113 | // 执行状态发生改变操作
114 | subject.setSubjectState("新的状态");
115 | })();
116 |
--------------------------------------------------------------------------------
/code/typescript/19_Flyweight.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 享元模式 (Flyweight)
3 | */
4 |
5 | /**
6 | * 享元接口,通过这个接口享元可以接受并作用于外部状态
7 | */
8 | interface Flyweight {
9 | /**
10 | * 示例操作,传入外部状态
11 | * @param extrinsicState 示例参数,外部状态
12 | */
13 | operation(extrinsicState: string): void;
14 | }
15 |
16 | /**
17 | * 享元对象
18 | */
19 | class ConcreteFlyweight implements Flyweight {
20 | /**
21 | * 示例,描述内部状态
22 | */
23 | private intrinsicState: string;
24 |
25 | /**
26 | * 构造方法,传入享元对象的内部状态的数据
27 | * @param state 享元对象的内部状态的数据
28 | */
29 | public constructor(state: string) {
30 | this.intrinsicState = state;
31 | }
32 |
33 | public operation(extrinsicState: string): void {
34 | // 具体的功能处理,可能会用到享元内部、外部的状态
35 | }
36 | }
37 |
38 | /**
39 | * 不需要共享的 flyweight 对象,
40 | * 通常是被共享的享元对象作为子节点组合出来的对象
41 | */
42 | class UnsharedConcreteFlyweight implements Flyweight {
43 | /**
44 | * 示例,描述对象的状态
45 | */
46 | private allState: string;
47 |
48 | public operation(extrinsicState: string): void {
49 | // 具体的功能处理
50 | }
51 | }
52 |
53 | /**
54 | * 享元工厂
55 | *
56 | * 在享元模式中,客户端不能直接创建共享的享元对象实例,
57 | * 必须通过享元工厂来创建。
58 | */
59 | class FlyweightFactory {
60 | /**
61 | * 通常实现为单例
62 | */
63 | private static factory = new FlyweightFactory();
64 |
65 | public static getInstance(): FlyweightFactory {
66 | return FlyweightFactory.factory;
67 | }
68 |
69 | /**
70 | * 缓存多个 Flyweight 对象,这里只是示意一下
71 | */
72 | private fsMap: { [key: string]: Flyweight } = {};
73 |
74 | /**
75 | * 获取 Key 对应的享元对象
76 | * @param key 获取享元对象的 key,只是示意
77 | * @return key 对应的享元对象
78 | */
79 | public getFlyweight(key: string): Flyweight {
80 | // 这个方法基本的实现步骤如下:
81 | // 1:先从缓存中查找,是否存在 key 对应的 Flyweight 对象
82 | let f: Flyweight = this.fsMap[key];
83 |
84 | // 2:如果存在,就返回相应的 Flyweight 对象
85 | if (!f) {
86 | // 3:如果不存在
87 | // 3.1:创建一个心的 Flyweight 对象
88 | f = new ConcreteFlyweight(key);
89 | // 3.2:把这个新的 Flyweight 对象添加到缓存中
90 | this.fsMap[key] = f;
91 | // 3.3:然后返回这个新的 Flyweight 对象
92 | }
93 |
94 | return f;
95 | }
96 | }
97 |
98 | /**
99 | * 客户端
100 | */
101 | ;(() => {
102 | const fw1: Flyweight = FlyweightFactory.getInstance().getFlyweight("username-1");
103 | fw1.operation("params");
104 |
105 | const fw2: Flyweight = FlyweightFactory.getInstance().getFlyweight("username-2");
106 | fw2.operation("params")
107 |
108 | const fw1_B: Flyweight = FlyweightFactory.getInstance().getFlyweight("username-1");
109 | fw1_B.operation("params")
110 | })();
111 |
--------------------------------------------------------------------------------
/code/typescript/09_Mediator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 中介者模式 (Mediator)
3 | *
4 | * 「封装交互」
5 | */
6 |
7 | /**
8 | * 同事类的抽象父类
9 | */
10 | abstract class Colleague {
11 | /**
12 | * 持有中介者对象,每一个同事类都知道它的中介者对象
13 | */
14 | private mediator: Mediator;
15 |
16 | /**
17 | * 构造方法,传入中介者对象
18 | * @param mediator 中介者对象
19 | */
20 | public constructor(mediator: Mediator) {
21 | this.mediator = mediator;
22 | }
23 |
24 | /**
25 | * 获取当前同事类对应的中介者对象
26 | * @return 对应的中介者对象
27 | */
28 | public getMediator(): Mediator {
29 | return this.mediator;
30 | }
31 | }
32 |
33 | /**
34 | * 具体的同事类 A
35 | */
36 | class ConcreteColleagueA extends Colleague {
37 | public constructor(mediator: Mediator) {
38 | super(mediator);
39 | }
40 |
41 | /**
42 | * 示例方法,执行某些业务功能
43 | */
44 | public someOperation(): void {
45 | // 在需要跟其他同事通信的时候,通知中介者对象
46 | this.getMediator().changed(this);
47 | }
48 | }
49 |
50 | /**
51 | * 具体的同事类 B
52 | */
53 | class ConcreteColleagueB extends Colleague {
54 | public constructor(mediator: Mediator) {
55 | super(mediator);
56 | }
57 |
58 | /**
59 | * 示意方法,执行某些业务功能
60 | */
61 | public someOperation(): void {
62 | // 在需要跟其他同事通信的时候,通知中介者对象
63 | this.getMediator().changed(this);
64 | }
65 | }
66 |
67 | /**
68 | * 中介者,定义各个同事对象通知的接口
69 | */
70 | interface Mediator {
71 | /**
72 | * 同事对象在自身改变的时候来通知中介者的方法
73 | * 让中介者去负责相应的与其他同事对象的交互
74 | * @param colleague 同事对象自身,好让中介者对象通过对象实例
75 | * 去获取同事对象的状态
76 | */
77 | changed(colleague: Colleague): void;
78 | }
79 |
80 | class ConcreteMediator implements Mediator {
81 | /**
82 | * 持有并维护同事 A
83 | */
84 | private colleagueA: ConcreteColleagueA;
85 |
86 | /**
87 | * 持有并维护同事 B
88 | */
89 | private colleagueB: ConcreteColleagueB;
90 |
91 | /**
92 | * 设置中介者需要了解并维护的同事 A 对象
93 | * @param colleague 同事 A 对象
94 | */
95 | public setConcreteColleagueA(colleague: ConcreteColleagueA): void {
96 | this.colleagueA = colleague;
97 | }
98 |
99 | /**
100 | * 设置中介者需要了解并维护的同事 B 对象
101 | * @param colleague 同事 B 对象
102 | */
103 | public setConcreteColleagueB(colleague: ConcreteColleagueB) {
104 | this.colleagueB = colleague;
105 | }
106 |
107 | public changed(colleague: Colleague): void {
108 | // 某个同事类发生了变化,通常需要与其他同事交互
109 | // 具体协调相应的同事对象来实现协作行为
110 | }
111 | }
112 |
113 | /**
114 | * 客户端
115 | */
116 | ;(() => {
117 | // 创建中介者对象
118 | const mediator = new ConcreteMediator();
119 |
120 | // 创建同事类
121 | const colleagueA = new ConcreteColleagueA(mediator);
122 | const colleagueB = new ConcreteColleagueB(mediator);
123 |
124 | // 让中介者知道所有的同事
125 | mediator.setConcreteColleagueA(colleagueA);
126 | mediator.setConcreteColleagueB(colleagueB);
127 |
128 | // 执行操作
129 | colleagueA.someOperation()
130 | colleagueB.someOperation()
131 | })();
132 |
--------------------------------------------------------------------------------
/code/typescript/14_Composite.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 组合模式 (Composite)
3 | *
4 | * 「统一叶子对象和组合对象」
5 | */
6 |
7 | /**
8 | * 抽象的组件对象,为组合中的对象声明接口,实现接口的缺省行为
9 | */
10 | abstract class Component {
11 | /**
12 | * 示意方法,子组件对象可能有的功能方法
13 | */
14 | public abstract someOperation(): void;
15 |
16 | /**
17 | * 向组合对象中加入组件对象
18 | * @param child 被加入组合对象中的组件对象
19 | */
20 | public addChild(child: Component): void {
21 | // 缺省的实现,抛出例外,因为叶子对象没有这个功能
22 | // 或者子组件没有实现这个功能
23 | throw new Error("对象不支持这个功能");
24 | }
25 |
26 | /**
27 | * 从组合对象中移出某个组件对象
28 | * @param child 被移出的组件对象
29 | */
30 | public removeChild(child: Component) {
31 | // 缺省的实现,抛出例外,因为叶子对象没有这个功能
32 | // 或者子组件没有实现这个功能
33 | throw new Error("对象不支持这个功能");
34 | }
35 |
36 | /**
37 | * 返回某个索引对应的组件对象
38 | * @param index 需要获取的组件对象的索引,索引从 0 开始
39 | * @return 索引对应的组件对象
40 | */
41 | public getChildren(index: number): Component {
42 | // 缺省的实现,抛出例外,因为叶子没有这个功能
43 | // 或者子组件没有实现这个功能
44 | throw new Error("对象不支持这个功能");
45 | }
46 | }
47 |
48 | /**
49 | * 组合对象,通常需要存储子对象,定义有子部件的部件行为
50 | * 并实现在 Component 里面定义的与子组件有关的操作
51 | */
52 | class Composite extends Component {
53 | /**
54 | * 用来存储组合对象中包含的子组件对象
55 | */
56 | private childComponents: Component[] = null;
57 |
58 | /**
59 | * 示意方法,通常在里面需要实现递归调用
60 | */
61 | public someOperation(): void {
62 | if (this.childComponents !== null) {
63 | this.childComponents.forEach((c) => {
64 | // 递归地进行子组件相应方法的调用
65 | c.someOperation();
66 | })
67 | }
68 | }
69 |
70 | public addChild(child: Component): void {
71 | // 延迟初始化
72 | if (this.childComponents == null) {
73 | this.childComponents = [];
74 | }
75 |
76 | this.childComponents.push(child);
77 | }
78 |
79 | public removeChild(child: Component): void {
80 | if (this.childComponents !== null) {
81 | this.childComponents.splice(this.childComponents.indexOf(child), 1);
82 | }
83 | }
84 |
85 | public getChildren(index: number): Component {
86 | if (this.childComponents !== null) {
87 | if (index >= 0 && index < this.childComponents.length) {
88 | return this.childComponents[index];
89 | }
90 |
91 | return null;
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * 叶子对象,叶子对象不再包含其他子对象
98 | */
99 | class Leaf extends Component {
100 | /**
101 | * 示意方法,叶子对象可能有自己的功能方法
102 | */
103 | public someOperation(): void {
104 | // do something
105 | }
106 | }
107 |
108 | /**
109 | * 客户端
110 | */
111 | ;(() => {
112 | // 定义多个 Composite 对象
113 | const root: Component = new Composite();
114 | const c1: Component = new Composite();
115 | const c2: Component = new Composite();
116 |
117 | // 定义多个叶子对象
118 | const leaf1: Component = new Leaf();
119 | const leaf2: Component = new Leaf();
120 | const leaf3: Component = new Leaf();
121 |
122 | // 组合成为树形的对象结构
123 | root.addChild(c1);
124 | root.addChild(c2);
125 | root.addChild(leaf1);
126 | c1.addChild(leaf2);
127 | c1.addChild(leaf3);
128 |
129 | // 操作 Component 对象
130 | const o: Component = root.getChildren(1);
131 | console.log(o);
132 | })();
133 |
--------------------------------------------------------------------------------
/code/typescript/13_Iterator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 迭代器模式 (Iterator)
3 | *
4 | * 「控制访问聚合对象的元素」
5 | */
6 |
7 | /**
8 | * 迭代器接口,定义访问和遍历元素的操作
9 | */
10 | interface IteratorAPI {
11 | /**
12 | * 移动到聚合对象的某一个位置
13 | */
14 | first(): void;
15 |
16 | /**
17 | * 移动到聚合对象的下一个位置
18 | */
19 | next(): void;
20 |
21 | /**
22 | * 判断是否已经移动到聚合对象的最后一个位置
23 | */
24 | isDone(): boolean;
25 |
26 | /**
27 | * 获取迭代的当前元素
28 | */
29 | currentItem(): any;
30 | }
31 |
32 | /**
33 | * 具体的迭代器实现对象,示意的是聚合对象为数组的迭代器
34 | * 不同的聚合对象相应的迭代器实现是不一样的
35 | */
36 | class ConcreteIterator implements IteratorAPI {
37 | /**
38 | * 持有被迭代的具体的聚合对象
39 | */
40 | private aggregate: ConcreteAggregate;
41 |
42 | /**
43 | * 内部索引,记录当前迭代到的索引位置
44 | * -1 表示刚开始的时候,迭代器指向聚合对象第一个对象之前
45 | */
46 | private index: number = -1;
47 |
48 | /**
49 | * 构造方法,传入被迭代的具体的聚合对象
50 | * @param aggregate 被迭代的具体的聚合对象
51 | */
52 | public constructor(aggregate: ConcreteAggregate) {
53 | this.aggregate = aggregate;
54 | }
55 |
56 | public first(): void {
57 | this.index = 0;
58 | }
59 |
60 | public next(): void {
61 | if (this.index < this.aggregate.size()) {
62 | this.index = this.index + 1;
63 | }
64 | }
65 |
66 | public isDone(): boolean {
67 | if (this.index === this.aggregate.size()) {
68 | return true;
69 | }
70 |
71 | return false;
72 | }
73 |
74 | public currentItem() {
75 | return this.aggregate.get(this.index);
76 | }
77 | }
78 |
79 | /**
80 | * 聚合对象的接口,定义创建相应迭代器对象的接口
81 | */
82 | abstract class Aggregate {
83 | /**
84 | * 工厂方法,创建相应迭代器对象的接口
85 | * @return 相应迭代器对象的接口
86 | */
87 | public abstract createIterator(): IteratorAPI;
88 | }
89 |
90 | /**
91 | * 具体的聚合对象,实现创建相应迭代器对象的功能
92 | */
93 | class ConcreteAggregate extends Aggregate {
94 | /**
95 | * 示意,表示聚合对象具体的内容
96 | */
97 | private ss: string[] = null;
98 |
99 | /**
100 | * 构造方法,传入构造对象具体内容
101 | * @param ss 聚合对象具体的内容
102 | */
103 | public constructor(ss: string[]) {
104 | super();
105 | this.ss = ss;
106 | }
107 |
108 | public createIterator(): IteratorAPI {
109 | // 实现创建 Iterator 的工厂方法
110 | return new ConcreteIterator(this);
111 | }
112 |
113 | /**
114 | * 获取索引所对应的元素
115 | * @param index 索引
116 | * @return 索引对应的元素
117 | */
118 | public get(index: number): any {
119 | let retObj: any = null;
120 | if (index < this.ss.length) {
121 | retObj = this.ss[index];
122 | }
123 | return retObj;
124 | }
125 |
126 | /**
127 | * 获取聚合对象的大小
128 | * @return 聚合对象的大小
129 | */
130 | public size(): number {
131 | return this.ss.length;
132 | }
133 | }
134 |
135 | /**
136 | * 客户端
137 | */
138 | ;(() => {
139 | /**
140 | * 示意方法,使用迭代器的功能
141 | * 这里示意使用迭代器来迭代聚合对象
142 | */
143 |
144 | const names: string[] = ["Kevin", "Tom", "Tony"];
145 |
146 | // 创建聚合对象
147 | const aggregate = new ConcreteAggregate(names);
148 |
149 | // 循环输出聚合对象中的值
150 | const it = aggregate.createIterator();
151 |
152 | // 首先设置迭代器到第一个元素
153 | it.first();
154 |
155 | while(!it.isDone()) {
156 | // 取出当前的元素
157 | const obj = it.currentItem();
158 | console.log("the obj is " + obj);
159 | // 如果还没有迭代到最后,那么就向下迭代一个
160 | it.next();
161 | }
162 | })();
163 |
--------------------------------------------------------------------------------
/1_简单工厂.md:
--------------------------------------------------------------------------------
1 | # 简单工厂
2 |
3 | ## 问题描述
4 |
5 | ```java
6 | public interface Api {
7 | public void test1(String s);
8 | }
9 |
10 | public class Impl implements Api {
11 | public void test1(String s) {
12 | System.out.println("Now In Impl. The input s==="+s);
13 | }
14 | }
15 |
16 | public class Client {
17 | public static void main(String[] args) {
18 | Api api = new Impl();
19 | api.test1("Java:面向接口的编程");
20 | }
21 | }
22 | ```
23 |
24 | 你会发现在客户端调用的时候,客户端不但知道了接口,同时还知道了具体实现就是 Impl。
25 | 接口的思想是“封装隔离”,而实现类 Impl 应该是被接口 Api 封装并同客户端隔离开的,
26 | 也就是说,客户端根本就不知道具体的实现类是 Impl。
27 |
28 | 问题:在 Java 编程中,出现只知道接口而不知道实现,该怎么办?
29 | 就像现在的 Client,他知道要使用 Api 接口,但是不知道该由谁来实现,也不知道如何实现,
30 | 从而得不到接口对象,就无法使用接口,该怎么办?
31 |
32 | 解决方案:使用简单工厂来解决问题。
33 |
34 | ## 解决方案
35 |
36 | 简单工厂的定义:
37 | 提供一个创建对象实例的功能,而无须关心其具体的实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。
38 |
39 | 通过以下方式达到了最重要的“封装隔离性”
40 |
41 | ```java
42 | public interface Api {
43 | public void operation(String s);
44 | }
45 |
46 | public class ImplA implements Api {
47 | public void operation(String s) {
48 | System.out.println("ImplA s=="+s);
49 | }
50 | }
51 |
52 | public class ImplB implements Api {
53 | public void operation(String s) {
54 | System.out.println("ImplB s=="+s);
55 | }
56 | }
57 |
58 | public class Factory {
59 | /**
60 | * 具体创建 Api 对象方法
61 | * @param condition 示意,从外部传入的选择条件
62 | * @return 创建好的 Api 对象
63 | */
64 | public static Api createApi(int condition) {
65 | // 应该根据某些条件去选择究竟创建哪一个具体的实现对象
66 | // 这些条件可以从外部传入,也可以从其他途径来获取。
67 | // 如果只有一个实现,可以省略条件,因为没选择的必要
68 | // 示意使用条件
69 | Api api = null;
70 | if (condition == 1) {
71 | api = new ImplA();
72 | } else if (condition == 2) {
73 | api = new ImplB();
74 | }
75 | return api;
76 | }
77 | }
78 |
79 | public class Client {
80 | public static void main(String[] args) {
81 | Api api = Factory.createApi(1);
82 | api.operation("正在使用简单工厂");
83 | }
84 | }
85 | ```
86 |
87 | 个人评论:实例是接口的实现,有很多接口的实现,但只知道接口,而不知道有哪些实例的情况下,
88 | 建立简单工厂类 Factory 为这些接口的实现提供一个实例化的方法,
89 | 通过调用这些方法就能得到 Api 接口的实现实例化对象
90 | 简而言之,就像是代码提示,提供候选项,可找到指定接口的实现
91 |
92 | 客户端那边不知道接口的具体实现是什么,只是调用 Factory 类里面的方法,得到自己想要的功能,想要的实例化对象
93 |
94 | 事实上,简单工厂能帮助我们真正地开始面向接口编程,
95 | 像以前的做法,其实是用到了接口多态部分的功能,最重要的“封装隔离性”并没有体现出来
96 |
97 | ## 模式讲解
98 |
99 | ### 典型疑问
100 |
101 | Q: 直接 Api api = new Impl() 和放到 Factory 里作为方法 createImpl 有什么区别?
102 |
103 | > 理解这个问题的重点在于理解简单工厂模式所处的位置。
104 |
105 | 简单工厂是位于封装体内部,目标就是不让客户端知道封装体内部的具体实现。
106 | 简单工厂的位置是位于封装体内的,也就是简单工厂跟接口和具体的实现在一起的,
107 | 算是封装体内部的一个类,所以简单工厂知道具体的实现类是没有关系的。
108 |
109 | 一个包装边界,表示接口、实现类和工厂类组合成一个组件。
110 | 在这个封装体里面,只有「接口」和「工厂」是对外的,
111 | 也就是让外部知道并使用,所以故意漏了一些在虚线外,
112 | 而具体的实现类是不对外的,被完全包含在虚线框内。
113 |
114 | 个人评论:
115 | 通过简单工厂模式才算是实现了封装的特性,达到了隔离的效果,
116 | 所以区别是显而易见的,一个包装了起来,一个是散的,没有实现隔离。
117 | 看似简单的 new Impl() 这句话从客户端移到简单工厂,其实是质的变化。
118 |
119 | [P20] 工厂不仅可以创建实例化对象,还可以创建其他东西 -> 万能工厂
120 |
121 | ## 简单工厂的优缺点
122 |
123 | 简单工厂有以下优点:
124 |
125 | - 帮助封装
126 | 简单工厂虽然很简单,但是非常友好地帮助我们实现了组件的封装,然后让组件外部能「真正」面向接口编程
127 | - 解耦
128 | 通过简单工厂,实现了客户端和具体实现类的解耦。
129 | 如同上面的例子,客户端根本就不知道具体是谁来实现,也不知道具体是如何实现,客户端只是通过工厂获取它需要的接口对象。
130 |
131 | 简单工厂有以下缺点:
132 |
133 | - 可增加客户端的复杂度
134 | 如果客户端的参数来选择具体的实现类,那么就必须让客户端能理解各个参数所代表的具体功能和含义,这样会增加客户端的使用难度,也部分暴露了内部实现,这种情况可以选用可配置的方法来实现。
135 | - 不方便扩展子工厂
136 | 私有化简单工厂的构造方法,使用静态方法来创建接口,也就不能通过写简单工厂类的子类来改变创建接口的方法行为了。不过,通常情况下是不需要为简单工厂创建子类的。
137 |
138 | ## 思考简单工厂
139 |
140 | 简单工厂的本质:选择实现
141 |
142 | - 注意简单工厂的重点在于选择,实现是已经做好了的。
143 | - 就算实现再简单,也要由具体实现类来实现,而不是在简单工厂里来实现。
144 | - 简单工厂的目的在于为客户端来选择相应的实现,从而使得客户端和实现之间解耦。
145 | 这样一来,具体实现发生了变化,就不用变动客户端了,这个变化会被简单工厂吸收和屏蔽掉。
146 |
147 | - 实现简单工厂的难点在于“如何选择”实现
148 | - 前面讲了几种传递参数的方法,那都是静态参数,还可以实现为「动态参数」
149 | 比如,在运行期间,由工厂去读取某个内存的值,或是去读取数据库中的值,然后根据这个值选择具体的实现等
150 |
151 | ### 何时选用简单工厂
152 |
153 | 建议在以下情况下:
154 |
155 | - 如果想要完全封装隔离具体的实现,让外部只能通过接口来操作封装体,那么可以选择简单工厂,让客户端通过工厂来获取相应的接口,而无需关心具体的实现
156 | - 如果想要把对外创建对象的职责集中管理和控制,可以选用简单工厂,一个简单工厂可以创建很多、不相关的对象,可以把对外创建对象的职责集中到一个简单工厂来,从而实现集中管理和控制。
157 |
158 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 设计模式
2 |
3 | [《研磨设计模式》](https://book.douban.com/subject/5343318/) 学习笔记 (by [@qwqcode](https://github.com/qwqcode))
4 |
5 | ## 学习进度
6 |
7 | [得分/10] 敲代码+1 理论阅读+1 理论评论(大概理解)+2 项目实践+1 自我感觉良好+5
8 |
9 | 1. [4] [简单工厂](./1_简单工厂.md)
10 | 2. [4] [外观模式 (Facade)](./2_外观模式.md)
11 | 3. [4] [适配器模式 (Adapter)](./3_适配器模式.md)
12 | 4. [6] [单例模式 (Singleton)](./4_单例模式.md)
13 | 5. [4] [工厂方法模式 (Factory Method)](./5_工厂方法模式.md)
14 | 6. [4] [抽象工厂模式 (Abstract Factory)](./6_抽象工厂模式.md)
15 | 7. [2] 生成器模式 (Builder)
16 | 8. [2] 原型模式 (Prototype)
17 | 9. [2] 中介者模式 (Mediator)
18 | 10. [2] 代理模式 (Proxy)
19 | 11. [2] 观察者模式 (Observer)
20 | 12. [2] 命令模式 (Command)
21 | 13. [2] 迭代器模式 (Iterator)
22 | 14. [2] 组合模式 (Composite)
23 | 15. [2] 模版方法模式 (Template Method)
24 | 16. [2] 策略模式 (Strategy)
25 | 17. [2] 状态模式 (State)
26 | 18. [] 备忘录模式 (Memento)
27 | 19. [2] 享元模式 (Flyweight)
28 | 20. [] 解释器模式 (Interpreter)
29 | 21. [] 装饰模式 (Decorator)
30 | 22. [2] 职责链模式 (Chian of Responsibility)
31 | 23. [2] 桥接模式 (Bridge)
32 | 24. [] 访问者模式 (Visitor)
33 |
34 | ## 扩展知识
35 |
36 | - 单例模式:
37 | - [延迟加载的思想](./4_单例模式_2.md#延迟加载的思想)
38 | - [缓存思想](./4_单例模式_2.md#缓存的思想)
39 | - [时间和空间问题](./4_单例模式_2.md#单例模式的优缺点)
40 | - [线程安全 (并发同步性能问题)](./4_单例模式_2.md#单例模式的优缺点)
41 | - [双重检查加锁](./4_单例模式_2.md#单例模式的优缺点)
42 | - [类级内部类](./4_单例模式_2.md#类级内部类(更好的方法))
43 | - [单例和枚举](./4_单例模式_2.md#单例和枚举(最佳方法))
44 | - 工厂方法模式:
45 | - [框架的基础知识](./5_工厂方法模式.md#框架的基础知识)
46 | - [IoC/DI (控制反转/依赖注入)](./5_工厂方法模式_2.md#工厂方法模式与-iocdi)
47 | - [平行的类层次结构](./5_工厂方法模式_2.md#平行的类层次结构)
48 | - [依赖倒置原则 (设计原则)](./5_工厂方法模式_2.md#2-对设计原则的实现)
49 | - 抽象工厂模式:
50 | - [DAO (数据访问对象)](./6_抽象工厂模式_2.md#抽象工厂模式和-DAO)
51 | - [LDAP (轻型访问协议)](./6_抽象工厂模式_2.md#抽象工厂模式和-DAO)
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 | 1. 对这些方法的理解和掌握
92 | 2. 然后进一步深化到这些方法所体现的思想上
93 | 3. 将设计模式所体现的思考方式进行吸收和消化
94 | 4. 融入到自己的思维中
95 |
96 | ### 学设计模式有何用处?
97 |
98 | 1. 设计模式成为软件开发人员的 "标准词汇"(交流所需)
99 | - 交流中像汉语成语一样使用
100 | - 说设计模式的名称知道思想
101 | - 一个合格的软件开发人员,必须掌握设计模式这个标准词汇
102 | 2. 学习设计模式是个人技术提高的途径
103 | - 站在巨人的肩膀上:前人积累的经验
104 | - 吸收领会前人的设计思想
105 | - 掌握前人解决问题的方法
106 | - 让自己技术提升
107 | 3. 不重复发明轮子
108 | - 设计模式对于特定问题能很好的解决,面对这些问题能快速解决
109 | - 节约大量研究时间,把时间花在其他问题上
110 |
111 | ### 学习设计模式的层次
112 |
113 | 1. 基本入门级
114 | - 正确理解和掌握设计模式的基本知识
115 | - 能够识别在什么场景下、出现了什么问题、采用何种方案来解决它
116 | - 能够在实际程序设计和开发中套用相应的设计模式
117 | 2. 基本掌握级
118 | - 具备基本的要求外,还能结合实际应用场景,对设计模式变形使用
119 | - 很多场景不一样的情况,适当变形,而不是僵硬套用
120 | - 进行变形前提是要能准确深入地理解和把握设计模式的本质
121 | - 万变不离其宗,只有把握本质,才能确保正确变形而不是误用
122 | 3. 深入理解和掌握级
123 | - 除了具备基本掌握级别外,更主要的是:
124 | - 从思想和方法上吸收设计模式的精髓,并融入到自己的思路中
125 | - 在进行软件的分析和设计的时候,能随意地、自然而然地应用,就如同自己思维的一部分
126 | - 深入考虑:除了设计模式外,考虑系统整体地结构、实际功能的实现、与已有功能的结合。在应用设计模式时,不拘泥于设计模式本身,而是从思想和方法层面进行应用
127 |
128 | 简而言之,基本入门套用使用,基本掌握灵活运用、适当变形,深入理解掌握才算真正将设计模式精髓吸收,从思想和方法层面去理解和掌握设计模式,就犹如练习武功到达最高境界,“无招胜有招”。要想达到这个境界,需要足够的开发经验和设计经验,没有足够深入的思考不太可能达到。
129 |
130 | 设计模式觉得懂了,实际运用还是不会。认为设计模式是 "看上去很美" 的 "花拳绣腿",其实他们正处于 "设计模式的了解级别",根本没有入门。
131 |
132 | ### 如何学习设计模式?
133 |
134 | - 心态:不要指望设计模式简单有趣,一看就懂(一看就懂属于科普性质的书)
135 | - 思考:看了过后思考,应用,再看,再思考,循环反复
136 | - 理论指导实践,实践反过来加深对理论的理解
137 | - 反复思考,反复实践
138 |
139 | 学习步骤:
140 |
141 | - 第一步:准确理解设计模式功能、基本结构、标准实现,了解适合它的场景及使用效果
142 | - 第二步:实际开发中,尝试使用这些设计模式,并反复思考和总结是否使用得当,是否需要做一些变化
143 | - 第三步:回头看设计模式的理论,有了实践再回头看会有不同感悟,一边看一边根据实践经验思考:设计模式的本质功能是什么?如何实现的?这些实现方式还可以在什么地方应用?如何才能把这个设计模式和具体应用结合起来?设计模式的出发点是什么?
144 | - 第四步:重复二三步。实践后再思考,循环反复,直到达到设计模式基本掌握水平
145 |
146 | 实际上,最后达到高度因人而异,需要看个人思维水平和理解水平。
147 |
148 | 建议是:反复地、深入地思考,别无他法。到了思想层面,就得靠 "悟" 了。
149 |
150 | 就我个人而言,为什么要学习设计模式?因为在项目实践过程中,很多代码的写法 (组织方式) 很好,我逐渐意识到这些方式方法就是我需要学习的「设计模式」
--------------------------------------------------------------------------------
/4_单例模式.md:
--------------------------------------------------------------------------------
1 | # 单例模式 (Singleton)
2 |
3 | ## 场景问题
4 |
5 | ### 读取配置文件的内容
6 |
7 | 考虑这样一个应用,读取配置文件的内容。
8 |
9 | 很多应用项目,都有与应用相关的配置文件,这些配置文件很多是由项目开发人员自定义的,在里面定义一些应用需要的参数数据。当然在实际的项目中,这种配置文件多采用 xml 格式,也有采用 properties 格式的,毕竟使用 Java 来读取 properties 格式的配置文件比较简单。
10 |
11 | 现在要读取配置文件的内容,该如何实现呢?
12 |
13 | ### 不用模式的解决方案
14 |
15 | 有些朋友会想,要读取配置文件的内容,这也是个困难的事情,直接读取文件的内容,然后把文件内容「存放在相应的数据对象里面」就可以了。真有那么简单吗?先实现看看吧。
16 |
17 | 为了示例简单,假设系统采用的是 properties 格式的配置文件。
18 |
19 | (1) 直接使用 Java 来读取配置文件的示例代码如下:
20 |
21 | ```java
22 | /**
23 | * 读取应用配置文件
24 | */
25 | public class AppConfig {
26 | /**
27 | * 用来存放配置文件中参数 A 的值
28 | */
29 | private String parameterA;
30 |
31 | /**
32 | * 用来存放配置文件中参数 B 的值
33 | */
34 | private String parameterB;
35 |
36 | public String getParameterA() {
37 | return parameterA;
38 | }
39 |
40 | public String getParameterB() {
41 | return parameterB;
42 | }
43 | // ↑ 注意:只有访问参数的方法,没有设置参数的方法
44 |
45 | /**
46 | * 构造方法
47 | */
48 | public AppConfig() {
49 | // 调用读取配置文件的方法
50 | readConfig();
51 | }
52 |
53 | /**
54 | * 读取配置文件,把配置文件中的内容读出来设置到属性上
55 | */
56 | public void readConfig() {
57 | Properties p = new Properties();
58 | InputStream in = null;
59 | try {
60 | in = AppConfig.class.getResourceAsStream("AppConfig.properties");
61 | p.load(in);
62 | // 把配置文件中的内容读出来设置到属性上
63 | this.parameterA = p.getProperty("paramA");
64 | this.parameterB = p.getProperty("paramB");
65 | } catch (IOException e) {
66 | System.out.println("装载配置文件出错了,具体堆栈信息如下:");
67 | e.printStackTrace();
68 | } finally {
69 | try {
70 | in.close();
71 | } catch (IOException e) {
72 | e.printStackTrace();
73 | }
74 | }
75 | }
76 | }
77 | ```
78 |
79 | (2) 应用的配置文件,名字是 AppConfig.properties,放在 AppConfig 相同的包里面。简单示例如下:
80 |
81 | ```config
82 | paramA=a
83 | paramB=b
84 | ```
85 |
86 | (3) 写个客户端来测试一下。示例代码如下:
87 |
88 | ```java
89 | public class Client {
90 | public static void main(String[] args) {
91 | // 创建读取应用配置的对象
92 | AppConfig config = new AppConfig();
93 |
94 | String paramA = config.getParameterA();
95 | String paramB = config.getParameterB();
96 |
97 | System.out.println("paramA="+paramA+",paramB="+paramB);
98 | }
99 | }
100 | ```
101 |
102 | 运行结果如下:
103 |
104 | ```
105 | paramA=a,paramB=b
106 | ```
107 |
108 | ### 有何问题
109 |
110 | 上面的实现很简单,很容易就实现要求的功能。仔细想想,有没有什么问题呢?
111 |
112 | 看看客户端「使用这个类的地方」,是「通过 new 一个 AppConfig 的实例」来得到一个操作配置文件内容的对象,也就是说「很多地方」都需要创建 AppConfig 对象的实例。
113 |
114 | 换句话说,在系统运行期间,系统中会存在「很多个 AppConfig 的实例对象」,这有什么问题吗?
115 |
116 | 当然有问题了,试想一下,每一个 AppConfig 实例对象里面「都封装着配置文件的内容」,系统中有「多个 AppConfig 对象实例」,也就是说系统中会「同时存在」多份配置文件内容,这样会「严重浪费内存资源」。如果配置文件内容较少,问题还小一点,如果配置文件内容本来就多的话,对于系统资源的浪费问题就大了。事实上,对于 AppConfig 这种类,在运行时期,「只需要一个」实例对象就是够了。
117 |
118 | 把上面的描述进一步抽象,问题就出来了:在一个系统运行时期,某个类「只需要一个类实例」就可以了,那么应该怎么实现呢?
119 |
120 | 评论:
121 | - 上述例子,我们如果不用单例模式,用户就能 new 多个相同功能的实例,最终会导致资源的严重浪费。
122 |
123 | ## 解决方案
124 |
125 | ### 使用单例模式来解决问题
126 |
127 | 用来解决上述问题的一个合理解决方案就是「单例模式 (Singleton)」。那么什么是单例模式呢?
128 |
129 | #### 1. 单例模式的定义
130 |
131 | > 保证一个类「仅有一个实例」,并提供一个访问它的「全局访问点」。
132 |
133 | #### 2. 应用单例模式来解决问题的思路
134 |
135 | 仔细分析上面的问题,现在一个类能够被创建多个实例,问题的根源在于「类的构造方法是公开的」,也就是可以让类的外部「通过构造方法创建多个实例」。换句说,只要「类的构造方法」能让类的「外部访问」,就「没有办法控制」外部来创建这个类的「实例个数」。
136 |
137 | 要想控制一个类「只被创建一个实例」,那么首要的问题就是「把创建实例的权限收回来」,让「类自身」来负责自己「类实例的创建工作」,然后由这个类来「提供外部可访问这个类实例的方法」,这就是单例模式的实现方法。
138 |
139 | 评论:
140 | - 收回外部可构造权限,可控制其实例数量
141 | - 自生负责为外部提供「可访问类实例的方法」,提供一个实例的「全局访问点」
142 |
143 | ### 单例模式的结构和说明
144 |
145 | 单例模式的结构图:
146 |
147 | 【图示】
148 |
149 | Singleton: 负责创建 Singleton 类自己的唯一实例,并「提供一个 getInstance 的方法」,让外部来访问这个类的唯一实例。
150 |
151 | ### 单例模式示例代码
152 |
153 | 在 Java 中,单例模式的实现又分为几种,一种称为「懒汉式」,一种称为「饿汉式」,其实就是在具体创建对象实例的处理上,有不同的实现方式。下面来分别看看这两种实现方式的代码示例。为何这么写,具体在后面再来讲述。
154 |
155 | (1) 懒汉式实现:
156 |
157 | ```java
158 | /**
159 | * 懒汉式单例实现代码如下:
160 | */
161 | public class Singleton {
162 | /**
163 | * 定义一个变量来存储创建好的类实例
164 | */
165 | private static Singleton uniqueInstance = null;
166 | // ↑ 用 static 修饰符来确保仅创建一个 instance
167 |
168 | /**
169 | * 私有化构造方法,可以再内部控制创建实例的数目
170 | */
171 | private Singleton() {
172 | //
173 | }
174 |
175 | /**
176 | * 定义一个方法来为客户端提供类实例
177 | * @return 一个 Singleton 的实例
178 | */
179 | public static synchronized Singleton getInstance() {
180 | // 判断存储实例的变量是否有值
181 | if (uniqueInstance == null) {
182 | // 如果没有,就创建一个类实例,并把值赋给存储类实例的变量
183 | uniqueInstance = new Singleton();
184 | }
185 |
186 | // 如果有值,那就直接使用
187 | return uniqueInstance;
188 | }
189 |
190 | /**
191 | * 实例方法,单例可以有自己的操作
192 | */
193 | public void singletonOperation() {
194 | // 功能处理
195 | }
196 |
197 | /**
198 | * 示意属性,单例可以有自己的属性
199 | */
200 | private String singletonData;
201 |
202 | /**
203 | * 示意方法,让外部通过这些方法来访问属性值
204 | */
205 | public String getSingletonData() {
206 | return singletonData;
207 | }
208 | }
209 | ```
210 |
211 | (2) 饿汉式实现:
212 |
213 |
214 | ```java
215 | /**
216 | * 懒汉式单例实现的示例
217 | */
218 | public class Singleton {
219 | /**
220 | * 定义一个变量来存储创建好的类示例,「直接在这里创建类实例,只能创建一次」
221 | */
222 | private static Singleton uniqueInstance = new Singleton();
223 |
224 | /**
225 | * 私有化构造方法,可以在内部控制创建实例的数量
226 | */
227 | private Singleton() {
228 | //
229 | }
230 |
231 | /**
232 | * 定义一个方法来为客户端提供类实例
233 | * @return 一个 Singleton 的实例
234 | */
235 | public static Singleton getInstance() {
236 | // 直接使用已创建好的实例
237 | return uniqueInstance;
238 | }
239 |
240 | /**
241 | * 示意方法,单例可以有自己的操作
242 | */
243 | public void singletonOperation() {
244 | // 功能处理
245 | }
246 |
247 | /**
248 | * 示意属性,单例可以有自己的属性
249 | */
250 | private String singletonData;
251 |
252 | /**
253 | * 示意方法,让外部通过这些方法来访问属性的值
254 | * @return 属性的值
255 | */
256 | public String getSingletonData() {
257 | return singletonData;
258 | }
259 | }
260 | ```
261 |
262 | 关于懒汉式、饿汉式的名称说明:
263 |
264 | 饿汉式、懒汉式其实是一种比较形象的称谓。
265 |
266 | 所谓的饿汉式,即饥饿,那么「在创建对象实例的时候就比较着急」,饿了嘛,于是就在装载类的时候就创建对象实例,写法如下:
267 |
268 | ```java
269 | private static Singleton uniqueInstance = new Singleton();
270 | ```
271 |
272 | 所谓懒汉式,既然是懒,那么「在创建对象实例的时候就不着急」,会「一直等到马上要使用对象实例的时候才会创建」,懒人嘛,总是推托不开的时候才去真正执行工作,因此在装载对象的时候不创建对象实例,写法如下:
273 |
274 | ```java
275 | private static Singleton uniqueInstance = null;
276 | ```
277 |
278 | 延伸:而是等到第一次使用的时候,才去创建实例,也就是在 getInstance 方法里面去判断和创建。
279 |
280 | 评论:
281 |
282 | 饿汉式能够保证调用的时候立刻出结果,而不像懒汉式,当调用到的时候再去创建,创建时会读取文件内容,解析等操作,需要耗时。在某些时候,例如一个用户请求一个页面的时候,如果用懒汉式就会多一些等待时间,而饿汉式立刻出结果,仅在程序初始化时耗时,不会影响用户体验,但懒汉式能够最大程度地保证「按需随时调用」,饿汉式极端情况,可能程序创建时一直占用资源,但从来没使用过,饿汉式能最大程度地节省资源。空间换时间,时间换空间的问题,需要权衡选择。
283 |
284 | ### 使用单例模式重写示例
285 |
286 | 由于单例模式有两种实现方式,这里选择一种来实现就可以了,我们选择「饿汉式」的实现方式来重写示例吧。
287 |
288 | ```java
289 | /**
290 | * 读取应用配置文件,单例实现
291 | */
292 | public class AppConfig {
293 | /**
294 | * 定义一个变量来存储创建好的类实例,直接在这里创建类实例,只创建一次
295 | */
296 | private static AppConfig instance = new AppConfig();
297 |
298 | /**
299 | * 定义一个方法来为客户端提供 AppConfig 类的实例
300 | * @return 一个 AppConfig 的实例
301 | */
302 | public static AppConfig getInstance() {
303 | return instance;
304 | }
305 |
306 | /**
307 | * 用来存放配置文件参数 A 的值
308 | */
309 | private String parameterA;
310 |
311 | /**
312 | * 用来存放配置文件参数 B 的值
313 | */
314 | private String parameterB;
315 |
316 | public String getParameterA() {
317 | return parameterA;
318 | }
319 |
320 | public String getParameterB() {
321 | return parameterB;
322 | }
323 |
324 | /**
325 | * 私有化构造方法
326 | */
327 | private AppConfig() {
328 | // 调用读取配置文件的方法
329 | readConfig();
330 | }
331 |
332 | /**
333 | * 读取配置文件,把配置文件中的内容读取出来设置到属性上
334 | */
335 | private void readConfig() {
336 | Properties p = new Properties();
337 | InputStream in = null;
338 | try {
339 | in = AppConfig.class.getResourceAsStream("AppConfig.properties");
340 | p.load(in);
341 | // 把配置文件中的内容读出来设置到属性上
342 | this.parameterA = p.getProperty("paramA");
343 | this.parameterB = p.getProperty("paramB");
344 | } catch (IOException e) {
345 | System.out.println("装载配置文件出错了,具体堆栈信息如下:");
346 | e.printStackTrace();
347 | } finally {
348 | try {
349 | in.close();
350 | } catch (IOException e) {
351 | e.printStackTrace();
352 | }
353 | }
354 | }
355 | }
356 | ```
357 |
358 | 当然,测试的客户端也需要相应地变化。实例代码如下:
359 |
360 | ```java
361 | public class Client {
362 | public static void main(String[] args) {
363 | // 创建读取应用配置的对象
364 | AppConfig config = AppConfig.getInstance();
365 |
366 | String paramA = config.getParameterA();
367 | String paramB = config.getParameterB();
368 |
369 | System.out.println("paramA="+paramA+",paramB="+paramB);
370 | }
371 | }
372 | ```
373 |
374 | 转到:[单例模式 (下)](./4_单例模式_2.md)
--------------------------------------------------------------------------------
/3_适配器模式_2.md:
--------------------------------------------------------------------------------
1 | ## 模式讲解
2 |
3 | ### 认识适配器模式
4 |
5 | #### 1. 模式的功能
6 |
7 | - 适配器模式的主要功能是「转换匹配」,目标是「复用已有的功能」,而不是实现新的接口。
8 | - 也就是说,客户端需要的功能应该是「已经实现好了的」,「不需要」适配器模式来实现。
9 | - 适配器模式主要负责把「不兼容的接口转换成客户端期望的样子」就可以了。
10 |
11 | 但这并不是说:在适配器里面就不能实现功能。适配器「可以实现功能」,称这种适配器为「智能适配器」。
12 |
13 | 再说了,在「接口匹配」和「转换」的过程中,也有「需要额外实现一些特定的功能」,才能够转换过来,比如需要调用参数以进行匹配等。
14 |
15 | #### 2. Adaptee 和 Target 的关系
16 |
17 | 适配器模式中,被适配的接口 Adaptee 和 适配成为的接口 Target 是「没有关系的」,也就是说,Adaptee 和 Target 中的「方法既可以相同,也可以不同」。
18 |
19 | 极端情况下两个接口里面的方法可能是「完全不同」的,但也可以是「完全相同」的。
20 |
21 | 这里所说的「相同和不同」,是指方法定义的「名称、参数列表、返回值」,以及「本身的功能」都可以相同或不同。
22 |
23 | #### 3. 对象组合
24 |
25 | 根据前面的实现,你会发现,适配器的实现方式其实是依靠「对象组合」的方式。
26 |
27 | 通过给适配器对象「组合被适配的对象」,然后当客户端调用 Target 的时候,适配器会把相应的功能「委托给被适配的对象去完成」。
28 |
29 | #### 4. 适配器模式的调用顺序示意图
30 |
31 | [P61]
32 |
33 | 评论:
34 |
35 | - 适配器的主要功能是:转换匹配。
36 | - 目的是:转换「已经实现」的功能,让客户端能够用「以前原来的方式」,调用到「被适配对象 (Adaptee)」。
37 | - 这个客户端期望的方式 (以前的方式 Target),原本就存在,而不是适配器 (Adapter) 新定义的。
38 | - Adaptee 和 Target 是「没有关系」的,两个 interface 可以完全相同,可以完全不同。
39 | - Adapter 的功能就是将 Adaptee 适配成 Target 接口的样子,实现「转换匹配」。
40 |
41 | ### 适配器模式的实现
42 |
43 | #### 1. 适配器的常见实现
44 |
45 | - 在实现适配器的时候,适配器通常是「一个类」,一般会让适配器类「去实现 Target 接口」,
46 | - 然后在适配器的具体实现里面调用 Adaptee。
47 | - 也就是说适配器通常是一个 Target 类型,而不是 Adaptee 类型,如同前面例子演示的那样。
48 |
49 | #### 2. 智能适配器
50 |
51 | - 在实际开发中,适配器也可以实现一些「Adaptee 没有实现,但是在 Target 中定义的功能」。
52 | - 这种情况就需要在适配器的实现里面,「加入新功能」的实现。
53 |
54 | 这种适配器被称为「智能适配器」。
55 |
56 | - 如果要使用智能适配器,一般新加入功能的实现会用到很多 Adaptee 的功能,
57 | - 「相当于利用 Adaptee 的功能来实现「更高层的」功能」。
58 | - 当然也可以完全实现「新加入的功能」,和已有功能都不相关,变相地「扩展了功能」。
59 |
60 | #### 3. 适配多个 Adaptee 「打星号」
61 |
62 | 适配器在适配的时候,可以适配多个 Adaptee,也就是说实现某个新的 Target 的功能的时候,可以调用多个模块的功能,适配多个模块的功能才能满足新接口的要求。
63 |
64 | 评论:
65 |
66 | - 在 new Adapter() 的时候,传递的参数可以是多个 Adaptee,例如 new Adapter(adaptee1, adaptee2, adaptee3),
67 | - 然后在 Adapter 的实现代码里调用多个 adaptee,最后才能满足 Target 的要求。
68 | - (注:这个 Adapter 的类型是 Target)
69 |
70 |
71 | #### 4. 适配器 Adapter 实现的复杂程度
72 |
73 | 适配器 Adapter 实现的复杂程度取决于 Target 和 Adaptee 的相似程度。(Adapter 的目的本身就是实现「转换」嘛)
74 |
75 | - 如果相似程度高:比如只有方法名称不一样,那么 Adapter 只需要「简单地转调一下」接口就可以了。
76 | - 如果相似程度低:比如两边接口方法所定义的功能完全不一样,在 Target 中定义的一个方法,可能在 Adaptee 中定义了「三个更小的」方法,那么这个时候在实现 Adapter 的时候,就需要「组合调用」了。
77 |
78 | 评论:我觉得如果 Adaptee 通过 Adapter 转换成 Target,如果 Adapter 实现太复杂了,还不如直接重新写一个全新的 Adaptee 来得快。但 Adapter 本来的目的就是为了实现「复用」,对原本存在的 Adaptee 进行复用。
79 |
80 | #### 5. 缺省适配 「打星号」
81 |
82 | 缺省适配的意思是:
83 |
84 | - 为一个「接口」提供「缺省实现」。
85 | - 有了它,就「不用直接去实现接口」,而是采用「继承」这个缺省适配对象,从而让子类可以「有选择地」去「覆盖实现」需要的方法,对于「不需要的方法」,使用「缺省适配」的方法就可以了。
86 |
87 | [P62]
88 |
89 | 评论:感觉有点抽象,得看看具体代码。
90 |
91 | ### 双向适配器
92 |
93 | 适配器也可以实现双向的适配,前面我们讲的都是把 Adaptee 适配成为 Target,其实也可以「把 Target 适配成为 Adaptee」。
94 |
95 | 也就是说:这个适配器可以「同时」当作 Target 和 Adaptee 来使用。
96 |
97 | 继续前面讲述例子,如果说由于某些原因,第一版和第二版会「同时共存」一段时间,比如第二版应用还在不断调整中,也就是第二版还不够稳定。客户提出,希望在两版共存期间,「主要还是使用第一版」,同时希望第一版日志「也能记录到数据库中」,也就是客户端虽然操作接口是「第一版的日志接口」,界面也是「第一版的界面」,但是可以使用第二版将日志记录到数据库的功能。(~~评论:哈哈,需求真 tm 多~~)
98 |
99 | 也就是说希望两版能实现「双向的匹配」,结构如图:
100 |
101 | 
102 |
103 | 下面用简单的代码示意一下,以便理解。
104 |
105 | 这里只加了几个新东西,一个是 DB 存储日志的实现,前面的例子中没有,因为「直接被适配成使用文件存储日志」的实现了;另外一个就是双向适配器,其实与把文件存储的方式适配成为 DB 实现的接口是一样的,只需要新「加上把 DB 实现的功能适配成为文件实现」的接口就可以了。(之前是文件适配成 DB,反过来再加上 DB 适配成文件,就实现了双向)
106 |
107 | (1) 先看看 DB 存储日志的实现。为了简单,这里不再真正地实现和数据库交互了,示意一下就可以了。
108 |
109 | 示例代码如下:
110 |
111 | ```java
112 | /**
113 | * DB 存储日志的实现,为了简单,这里简单示意一下
114 | */
115 | public class LogDbOperate implements LogDbOperateApi {
116 | public void createLog(LogModel lm) {
117 | System.out.println("now in LogDbOperate getAllLog");
118 | }
119 |
120 | public List getAllLog() {
121 | System.out.println("now in LogDbOperate getAllLog");
122 | return null;
123 | }
124 |
125 | public void removeLog(LogModel lm) {
126 | System.out.println("now in LogDbOperate removeLog,lm="+lm);
127 | }
128 |
129 | public void updateLog(LogModel lm) {
130 | System.out.println("now in LogDbOperate updateLog,lm="+lm);
131 | }
132 | }
133 | ```
134 |
135 | (2) 然后看看新的适配器的实现。
136 |
137 | 由于是「双向适配器」,
138 |
139 | - 一个方向是:把「新的 DB 实现的接口」适配成为「旧的文件操作需要的接口」;
140 | - 另一个方向是:把「旧的文件操作的接口」适配成为「新的 DB 实现需要的接口」。
141 |
142 | 示例代码如下:
143 |
144 | ```java
145 | /**
146 | * 双向适配器对象
147 | */
148 | public class TwoDirectAdapter implements LogDbOperateApi, LogFileOperateApi {
149 | // ↑ 这里同时实现需要适配的两个接口,Java 支持多个接口的实现
150 |
151 | // 但如果是其它语言,例如 Go,怎么实现两个接口呢?
152 | // 思考)所以说,设计模式实现的方法依赖于编程语言
153 | // ,但思想是通用的
154 | // ,但某些设计模式有时用到某些语言上,似乎不太行
155 |
156 | /**
157 | * 持有需要被适配的文件存储日志的接口对象
158 | */
159 | private LogFileOperateApi fileLog;
160 |
161 | /**
162 | * 持有需要被适配的 DB 存储日志的接口对象
163 | */
164 | private LogDbOperateApi dbLog;
165 |
166 | /**
167 | * 构造方法,传入需要被适配的对象
168 | * @param fileLog 需要被适配的文件日志对象
169 | * @param dbLog 需要被适配的 DB 日志对象
170 | */
171 | public TwoDirectAdapter(LogFileOperateApi fileLog, LogDbOperateApi dbLog) {
172 | this.fileLog = fileLog;
173 | this.dbLog = dbLog;
174 | }
175 |
176 | /*------------- 以下是把「文件」操作的方式适配成为「DB」实现方式的接口 -------------*/
177 | public void createLog(LogModel lm) {
178 | // 1:先读取文件内容
179 | List list = fileLog.readLogFile();
180 | // 2:加入新的日志对象
181 | list.add(lm);
182 | // 3:重新写入文件
183 | fileLog.writeLogFile(list);
184 | }
185 |
186 | public List getAllLog() {
187 | return fileLog.readLogFile();
188 | }
189 |
190 | public void removeLog(LogModel lm) {
191 | // 1:先读取文件的内容
192 | List list = fileLog.readLogFile();
193 | // 2:删除相应的日志对象
194 | list.remove(lm);
195 | // 3:重新写入文件
196 | fileLog.writeLogFile(list);
197 | }
198 |
199 | public void updateLog(LogModel lm) {
200 | // 1:先读取文件的内容
201 | List list = fileLog.readLogFile();
202 | // 2:修改相应的日志对象
203 | for (int i = 0; i < list.size(); i++) {
204 | if (list.get(i).getLogId().equals(lm.getLogId())) {
205 | list.set(i, lm);
206 | break;
207 | }
208 | }
209 | // 3:重新写入文件
210 | fileLog.writeLogFile(list);
211 | }
212 |
213 | /*------------- 以下是把「DB」操作的方式适配成为「文件」实现方式的接口 -------------*/
214 | public List readLogFile() {
215 | return dbLog.getAllLog();
216 | }
217 |
218 | public void writeLogFile(List list) {
219 | // 1:最简单的思路是先删除数据库中的数据
220 | // 2:然后循环把现在的数据加入到数据中
221 | for (LogModel lm : list) {
222 | dbLog.createLog(lm);
223 | }
224 | }
225 | }
226 | ```
227 |
228 | (3) 下面看看如何使用这个双向适配器。示例代码:
229 |
230 | ```java
231 | public class Client {
232 | public static void main(String[] args) {
233 | // 准备日志的内容,也就是测试的数据
234 | LogModel lml = new LogModel();
235 | lml.setLogId("001");
236 | lml.setOperateUser("qwqcode");
237 | lml.setOperateTime("2022-03-09 21:25:23");
238 | lml.setLogContent("这是一个测试 qwq qwq");
239 |
240 | List list = new ArrayList();
241 | list.add(lml);
242 |
243 | // 创建操作日志文件的对象
244 | LogFileOperateApi fileLogApi = new LogFileOperate();
245 | list.add(lml);
246 |
247 | // 创建操作双向适配后的操作日志的接口对象
248 | LogFileOperateApi fileLogApi2 = new TwoDirectAdapter(fileLogApi.dbLogApi);
249 | LogDbOperateApi dbLogApi2 = new TwoDirectAdapter(fileLogApi.dbLogApi);
250 |
251 | // 先测试从文件操作适配到第二版
252 | // 虽然调用的是第二版的接口,其实是文件操作在实现
253 | dbLogApi2.createLog(lm1);
254 | List allLog = dbLogApi2.getAllLog();
255 | System.out.println("allLog="+allLog);
256 |
257 | // 再测试从数据库适配成第一版的接口
258 | // 也就是调用第一版的接口,其实是数据库操作再实现
259 | fileLogApi2.writeLogFile(list);
260 | fileLogApi2.readLogFile();
261 | }
262 | }
263 | ```
264 |
265 | 运行一下,看看结果,体会以下双向适配器。
266 |
267 | 评论:
268 | - 实现「双向适配器」就是,创建一个类同时实现两个接口 A、B,也就是需要适配的接口。
269 | - 在这个类实例化的时候,同时传入接口 A、B 的实现 (adaptee),然后持有它 (保存为成员字段)。
270 | - 再这个类里面:
271 | - 实现 A 接口的方法,在方法中调用 B 接口实现的方法 (调用 B 的 adaptee),
272 | - 实现 B 接口的方法,在方法中调用 A 接口实现的方法 (调用 A 的 adaptee),。
273 | - 简而言之,就是为了 A、B 相互适配,A、B 相互转换,让这个适配器同时具有 A、B 接口定义的方法。
274 | - 客户端可以使用这个「双向适配器」,初始化这个类的时候,类型要么设置成 A,要么设置成 B。
275 | - 当这个双向适配器:
276 | - 类型为 A 时,可以操作 A 的方法,其实是对 B 的操作;(用 A 的接口操作 B)
277 | - 类型为 B 时,可以操作 B 的方法,其实是对 A 的操作。(用 B 的接口操作 A)
278 | - 这样实现调用 A、B interface 对功能的双向互相转换。
279 |
280 | 注意:事实上,使用适配器有一个「潜在的问题」,就是「被适配对象」不再兼容 Adaptee 的接口,因为「适配器只是实现了 Target 的接口」。这导致「并不是所有 Adaptee 对象可以被使用的地方」都能使用适配器。(P66)
281 |
282 | 而双向适配器就解决了这样的问题,双向适配器同时实现了 Target 和 Adaptee 的接口 (双向嘛,原来被适配对象的接口都实现了,可以反回去),使得双向适配器可以再 Target 和 Adaptee 被使用的地方使用,以「提供对所有客户的透明性」。尤其在两个「不同的客户端」需要用到「不同的方式」查看「同一个对象」时,需要使用双向适配器。「标星」
283 |
284 | 评论:
285 | - 提出问题:为什么要使用双向适配器?双向适配器相比于单向适配器的优点在于什么?什么场景下用双向适配器?
286 | - 双向适配器同时实现 Target 和 Adaptee,使得可以同时在已知 Target 和 Adaptee 的地方都能被使用,提供了对所有客户的「透明性」。
287 | - 使用双向适配器的好处尤其体现在,两个「不同客户端」需要用到「不同方式」查看「同一对象」时。
288 |
289 | ### 对象适配器和类适配器
290 |
291 | 在标准的适配器模式里面,根据适配器的实现方式,把适配器分成了两种:
292 |
293 | - 一种是**对象适配器**,
294 | - 另一种是**类适配器**。
295 |
296 | 对象适配器的实现:依赖于「对象组合」。就如同前面的实现示例,就是采用对象组合的方式,也就是「对象适配器」的方式。
297 |
298 | 类适配器的实现:采用「多重继承」对「一个接口」或与「另一个接口」进行匹配。
299 |
300 | 由于 Java 不支持多重继承,所以到目前为止还没有涉及。
301 |
302 | 1. 类适配器
303 |
304 | 前面已经学习过「对象适配器」了,下面简单地介绍一下类适配器。首先来看看类适配器的结构,如图:
305 |
306 | 
307 |
308 | 从结构图上可以看出,类适配器是通过「继承」来实现接口适配的,标准的设计模式中,类适配器是同时继承 Target 和 Adaptee 的,也就是一个「多重继承」,这在 Java 里面是不被支持的,也就是说 Java 中是不能实现标准的「类适配器模式」的。
309 |
310 | 但是 Java 中有一种变通的方式,也能够使用继承来实现接口的适配,那就是「让适配器去实现 Target 的接口」,然后「继承 Adaptee 的实现」,虽然「不是十分标准」,但是意思差不多。下面就来看个小示例。
311 |
312 | 2. Java 中类似实现类适配器的例子
313 |
314 | [P67] 暂时留着
315 |
316 | 3. 类适配器和对象适配器的权衡
317 |
318 | - 从实现上:类适配器使用对象继承的方式,是静态的定义方式;而对象适配器使用对象组合的方式,是动态组合的方式
319 | - 对于类适配器,由于适配器直接继承了 Adaptee,使得适配器不能和 Adaptee 的子类一起工作,因为继承是静态的关系,当适配器继承了 Adaptee 后,就不可以再去处理 Adaptee 的子类了。
320 | 对于对象适配器,允许一个 Adaptee 和多个 Adaptee,包括 Adaptee 和它所有的子类一起工作。因为对象适配器采用的是对象组合的关系,只要对象类型正确,就不是子类都无所谓。
321 | - 对于类适配器,适配器可以重定义 Adaptee 的部分行为,相当于子类覆盖父类的部分实现方法。
322 | 对于对象适配器,要重定义 Adaptee 的行为比较困难,这种情况下,需要定义 Adaptee 的子类来实现重定义,然后让适配器组合子类。
323 | - 对于类适配器,仅仅引入了一个对象,并不需要额外的应用来间接得到 Adaptee。
324 | 对于对象适配器,需要额外的引用来间接欸得到 Adaptee。
325 |
326 | 在 Java 开发中,建议大家尽量使用「对象适配器」的实现方式。当然,具体问题具体分析,根据需求来选用实现的方式,最适合的才是最好的。
327 |
328 | ## 适配器模式的优缺点
329 |
330 | **适配器模式有如下优点**:
331 |
332 | - 更好的复用性
333 | 如果功能是已经有了的,只是「接口不兼容」,那么通过适配器模式就可以让这些功能得到更好的复用。
334 | - 更好的可扩展性
335 | 在实现适配器功能的时候,可以「调用自己开发的功能」,从而「自然地」扩展系统的功能。(功能的扩充)
336 |
337 | **适配器模式有如下缺点**:
338 |
339 | - 过多地使用适配器,会让系统「非常凌乱」,不容易整体进行把握
340 |
341 | 评论:就像现实生活中,你用各种转接器,把 type-C 拓展坞插在电脑上,拓展坞只有个 VGA 口,再插一个 HDMI 转 VGA 的转接头,然而显示器只有 DVI 接口,但你拥有一根 HDMI 线,你可以再买一个 DVI 转 HDMI 的转接头,这样就实现了 VGA->HDMI->DVI->typeC。
342 |
343 | 再例如:
344 |
345 | 
346 |
347 | (~~又不是不能用~~)
348 |
349 | ## 思考适配器模式
350 |
351 | ### 1. 适配器模式的本质
352 |
353 | 适配器模式的本质是:「转换匹配,复用功能」
354 |
355 | 适配器通过「转换调用」已有的实现,从而能把「已有的实现」匹配成「需要的接口」,使之能「满足」客户端的需要。也就是说「转换匹配是手段」,而「复用已有的功能才是目的」。「标星」
356 |
357 | 在转换匹配的过程中,适配器还可以在「转换调用的前后」实现一些功能的处理,也就是「实现智能的适配」(功能扩充)。
358 |
359 | ### 2. 何时选用适配器
360 |
361 | 建议在以下情况中选用适配器模式:
362 |
363 | - 如果你想要使用一个「已经存在的类」,但是它的「接口不符合你的需求」,这种情况可以使用适配器模式,来把「已有的实现」转换成「你需要的接口」。
364 | - 如果「你想创建一个可以复用的类」,这个类可能「和一些不兼容的类一起工作」,这种情况可以使用适配器模式,到时候需要什么就适配什么。
365 | - 如果「你想使用一些已经存在的子类」,但是「不可能对每一个子类都进行匹配」,这种情况可以选用「对象适配器」,直接「适配这些子类的父类」就可以了。
366 |
367 | ## 相关模式
368 |
369 | **适配器模式与桥接模式**
370 |
371 | 其实这两个模式除了结构略为相似外,功能上完全不同。
372 | 适配器模式是把「两个或者多个接口的功能进行转换匹配」;而桥接模式是「让接口和实现部分相分离,以便它们可以相对独立地变化」。
373 |
374 | **适配器模式与装饰模式**
375 |
376 | 从某种意义上来讲,适配器模式能「模拟实现简单的装饰模式的功能」,也就是「为已有功能添加功能」。比如我们在适配器里面这么些:
377 |
378 | ```java
379 | public void adapterMethod() {
380 | System.out.println("在调用 Adaptee 的方法之前完成一定的工作");
381 | // 调用 Adaptee 的相关方法
382 | adaptee.method();
383 | System.out.println("在调用 Adaptee 的方法之后完成一定的功能");
384 | }
385 | ```
386 |
387 | 如上的写法,就相当于在调用 Adaptee 的「被适配方法前后添加了新的功能」,这样适配过后,客户端得到的功能就「不单纯」是 Adaptee 的被适配方法的功能了。看看是不是「类似装饰模式」的功能呢?
388 |
389 | 注意:仅仅是类似,造成这种类似的原因是:两种设计模式在实现上都是使用「对象组合」,都可以在「转调组合对象功能」的「前后进行一些附加的处理」,因此有这么一个相似性。它们的「目的和本质是不一样的」。
390 |
391 | 两个模式有一个很大的不同:一般适配器适「配过后是需要改变接口的」,如果不改变接口就没有必要适配了;而装饰模式「不改变接口」,无论多少层装饰「都是一个接口」。因此装饰模式可以很容易地「支持递归组合」,而适配器就做不到,每次的接口不同,无法递归。
392 |
393 | **适配器模式和代理模式**
394 |
395 | 适配器模式可以和代理模式「组合使用」。在实现适配器的时候,可以通过代理来调用 Adaptee,这样可以「获得更大的灵活性」。(妙啊,中间再插入一层)
396 |
397 | **适配器模式和抽象工厂模式**
398 |
399 | 在适配器实现的时候,通常需要「得到被适配的对象」。如果被适配的是一个接口,那么就可以「组合一些」可以「创造对象实例」的设计模式,来「得到」被适配的对象实例,比如「抽象工厂模式、单例模式、工厂方法模式」等。
400 |
401 |
--------------------------------------------------------------------------------
/3_适配器模式.md:
--------------------------------------------------------------------------------
1 | # 适配器模式 (Adapter)
2 |
3 | ## 场景问题
4 |
5 | ### 电脑装机的例子
6 |
7 | 给电脑加个新的硬盘,电脑电源没有剩余的 SATA 供电线了,怎么办?可以使用「转接线」,大 4PIN 供电转 SATA。
8 |
9 |
10 |
11 | 把原有的接口转换来适应新需要的接口,实现一个「转接线类」 —— 适配器 (Adapter)。
12 |
13 | ### 同时支持「数据库」和「文件」的日志管理
14 |
15 | 还有生活中很多例子,例如各种管道的「转接头」,不同制式的「插座」等,能帮助理解适配器模式,但还是和应用系统开发有一定差距。
16 |
17 | 感觉好像很轻松就理解了,但开发的时候就不知道如何使用这个模式了,有些「隔靴搔痒」的感觉。
18 |
19 | 通过以下开发中的例子,进一步认识适配器模式:
20 |
21 | 用户对日志记录的要求很高,有些时候不得不自己写一个日志工具或日志框架来满足特定需求。
22 |
23 | #### 1. 日志管理第一版
24 |
25 | 在第一版的时候,用户要求日志以「文件」的形式记录。开发人员遵循用户的要求,对日志文件的存取实现如下。
26 |
27 | (1) 首先需要简单定义日志对象,也就是描述日志的对象模型。由于这个对象需要被写入文件中,因此这个对象需要「序列化」。示例代码如下:
28 |
29 | ```java
30 | /**
31 | * 日志数据对象
32 | */
33 | public class LogModel {
34 | /* 日志编号 */
35 | private String logId;
36 |
37 | /* 操作人员 */
38 | private String operateUser;
39 |
40 | /* 操作时间 */
41 | private String operateTime;
42 |
43 | /* 操作内容 */
44 | private String logContent;
45 |
46 | public String getLogId() {
47 | return logId;
48 | }
49 |
50 | public void setLogId(String logId) {
51 | this.logId = logId;
52 | }
53 |
54 | // 各种 getter/setter....
55 | // ...
56 |
57 | public String toString() {
58 | return "logId="+logId+",operateUser="+operateUser+",operateTime="+operateTime+",logContent="+logContent;
59 | }
60 | }
61 | ```
62 |
63 | (2) 接下来定义一个操作日志文件的接口:
64 |
65 | ```java
66 | /**
67 | * 日志文件操作接口
68 | */
69 | public interface LogFileOperateApi {
70 | /**
71 | * 读取日志文件,从文件里面获取日志文件列表对象
72 | * @return 存储日志列表对象
73 | */
74 | public List readLogFile();
75 |
76 | /**
77 | * 写日志文件,把日志列表写出到日志文件中去
78 | * @param list 要写到日志文件的日志列表
79 | */
80 | public void writeLogFile(List list);
81 | }
82 | ```
83 |
84 | (3) 实现日志文件的存取。现在的实现也很简单,就是读写文件:
85 |
86 | ```java
87 | /**
88 | * 实现对日志文件的操作
89 | */
90 | public class LogFileOperate implements LogFileOperateApi {
91 | /**
92 | * 日志文件的路径和文件名称,默认是当前项目根目录下的 AdapterLog.log
93 | */
94 | private String logFilePathName = "AdapterLog.log";
95 |
96 | /**
97 | * 构造方法,传入文件的路径和名称
98 | */
99 | public LogFileOperate(String logFilePathName) {
100 | // 先判断是否传入了文件的路径和名称,如果是,
101 | // 就重新设置操作的日志文件的路径和名称
102 | if (logFilePathName != null && logFilePathName.trim().length() > 0) {
103 | this.logFilePathName = logFilePathName;
104 | }
105 | }
106 |
107 | public List readLogFile() {
108 | List list = null;
109 | ObjectInputStream oin = null;
110 | try {
111 | File f = new File(logFilePathName);
112 | if (f.exist()) {
113 | oin = new ObjectInputStream(
114 | new BufferedInputStream(
115 | new FileInputStream(f)
116 | )
117 | )
118 | list = (List)oin.readObject();
119 | }
120 | } catch (Exception e) {
121 | e.printStackTrace();
122 | } finally {
123 | try {
124 | if (oin != null) {
125 | oin.clone();
126 | }
127 | } catch (IOException e) {
128 | e.printStackTrace();
129 | }
130 | }
131 |
132 | return list;
133 | }
134 |
135 | public void writeLogFile(List list) {
136 | File f = new File(logFileName);
137 | ObjectOutputStream oout = null;
138 | try {
139 | oout = new ObjectOutputStream(
140 | new BufferedOutputStream(
141 | new FileOutputStream(f)
142 | )
143 | );
144 |
145 | oout.writeObject(null);
146 | } catch (IOException e) {
147 | e.printStackTrace();
148 | } finally {
149 | try {
150 | oout.clone();
151 | } catch (IOException e) {
152 | e.printStackTrace();
153 | }
154 | }
155 | }
156 | }
157 | ```
158 |
159 | (4) 写个客户端来测试以下,看看好用不。
160 |
161 | ```java
162 | public class Client {
163 | public static void main(String[] args) {
164 | // 准备日志内容,也就是测试的数据
165 | LogModel lml = new LogModel();
166 | lml.setLogId("001");
167 | lml.setOperateUser("admin");
168 | lml.setOperateTime("2021-03-02 10:08:18");
169 | lml.setLogContent("这是一个测试");
170 |
171 | List list = new ArrayList();
172 | list.add(lml);
173 |
174 | // 创建操作日志文件的对象
175 | LogFileOperateApi api = new LogFileOperate("");
176 |
177 | // 保存日志文件
178 | api.writeLogFile(list);
179 |
180 | // 读取日志文件内容
181 | List readLog = api.readLogFile();
182 | System.out.println("readLog="+readLog);
183 | }
184 | }
185 | ```
186 | 测试的结果如下:
187 |
188 | ```
189 | readLog=[logId=001,operateUser=admin,operateTime=2010-03-02]
190 | 10:08:18,logContent=这是一个测试]
191 | ```
192 |
193 | 至此简单的实现了用户的要求,把日志文件「保存到文件中」,并且「从文件把日志内容读取出来」,进行管理。
194 |
195 | 看上去很容易,对吧,别慌,接着来。
196 |
197 | #### 2. 日志管理第二版
198 |
199 | 用户使用日志管理第一版一段时间后,开始考虑升级系统 (加需求)。
200 |
201 | 决定采用「数据库」来管理日志。
202 |
203 | 很快,按照数据库日志管理也实现出来了,并定义了日志管理的操作接口,主要是正对日志的增删改查的方法。接口的示例代码如下:
204 |
205 | ```java
206 | /**
207 | * 定义操作日志的应用接口,为了示例的简单,只是简单地定义了增删改查的方法
208 | */
209 | public interface LogDbOperateApi {
210 | /**
211 | * 新增日志
212 | * @param lm 需要新增的日志对象
213 | */
214 | public void createLog(LogModel lm);
215 |
216 | /**
217 | * 修改日志
218 | * @param lm 需要修改的日志对象
219 | */
220 | public void updateLog(LogModel lm);
221 |
222 | /**
223 | * 删除日志
224 | * @param lm 需要删除的日志对象
225 | */
226 | public void updateLog(LogModel lm);
227 |
228 | /**
229 | * 删除日志
230 | * @param lm 需要删除的日志对象
231 | */
232 | public void removeLog(LogModel lm);
233 |
234 | /**
235 | * 获取所有的日志
236 | * @param 所有日志对象
237 | */
238 | public List getAllLog();
239 | }
240 | ```
241 |
242 | 对于使用数据库来保存日志的实现,这里就不去涉及了,总之知道有这么一个「实现」就可以了。
243 |
244 | 客户提出了新的要求,能不能让日志管理第二版实现「同时」支持「数据库存储」和「文件存储」两种方式?
245 |
246 | ## 有何问题
247 |
248 | 有朋友可能回想,这有什么困难呢,两种实现方式不都是已经实现了吗,合并起来不就可以了?
249 |
250 | 问题在于,现在的业务是「使用的第二版的接口」,直接使用第二版新加入的实现是没有问题的,第二版新加入了「保存日志到数据库」中;但是「对于已有」的实现方法,也就是在第一版中采取的存储方式,它的操作接口和第二版「不一样」,这就导致了现在客户端「无法以同样的方式」来直接使用第一版的实现。如图:
251 |
252 | 
253 |
254 | 这就意味着,想要同时支持文件和数据库存储两种方式,需要「额外」地做一些工作,才可以让第一版的实现适应新的业务需要。
255 |
256 | 有朋友可能会想,干脆按照第二版的接口要求来「重新实现一个」文件操作的对象不久可以了吗,这样做确实可以,但是何必要重新做已经完成的功能呢?应该想办法复用,而不是重新实现。
257 |
258 | 一种很容易想到的方式是「直接修改已有的第一版代码」。但这种方式是不太好的,如果直接修改第一版的代码,那么久可能会导致「其他依赖于这些实现的应用不能正常运行」,再说,有可能第一版和第二版的开发公司是不一样的,在第二版实现的时候,根本拿不到第一版的源代码。
259 |
260 | 
261 |
262 | ## 解决方案
263 |
264 | ### 使用适配器模式来解决问题
265 |
266 | 用来解决上述问题的一个合理的解决方案就是适配器模式。那么什么是适配器模式呢?
267 |
268 | #### 1. 适配器模式的定义
269 |
270 | > 将一个类的接口「转换成」客户端「希望」的「另一个接口」。
271 | >
272 | > 适配器模式使得原本由于「接口不兼容」而「不能一起」工作的那些类「可以一起」使用。
273 |
274 | (想到了 office 2007 兼容包,让旧版 office 能打开新的 .docx 文件;适配器也可以说是转化器吧)
275 |
276 | #### 2. 应用适配器模式来解决问题的思路
277 |
278 | 仔细分析上面的问题,问题的根源在于「接口的不兼容」,功能是基本实现了的。
279 |
280 | 也就是说,只要想办法「让两边的接口匹配起来」,就可以复用第一版的功能了。
281 |
282 | 按照适配器模式的实现方法,可以「定义一个类」来实现第二版的功能,然后再内部实现的时候,「转调第一版已经实现了的功能」,这样就可以通过「对象组合」的方式,既「复用了第一版已有的功能」,同时「又在接口上满足了第二版」调用的要求。
283 |
284 | ### 适配器模式的结构和说明
285 |
286 | 
287 |
288 | - Client: 客户端,调用自己需要领域接口 Target。
289 | - Target: 定义「客户端需要的」特定领域的「接口」。
290 | - Adaptee: 「已存在的接口」,通常能满足客户端的功能需求,但是「接口与客户端要求的」特定领域的「接口不一致」,需要「被适配」。
291 | - Adapter: 适配器,把 Adaptee 适配成为 Client 需要的 Target。
292 |
293 | ### 适配器模式示例代码
294 |
295 | (1) 先看看 Target 接口定义的示例代码如下:
296 |
297 | ```java
298 | /**
299 | * 定义「客户端」使用的接口,与「特定领域」相关
300 | */
301 | public interface Target {
302 | /**
303 | * 示意方法,客户端请求处理方法
304 | */
305 | public void request();
306 | }
307 | ```
308 |
309 | (2) 再看看需要被适配的对象定义。示例代码如下:
310 |
311 | ```java
312 | /**
313 | * 已存在的接口,这个接口需要「被适配」
314 | */
315 | public class Adaptee {
316 | /**
317 | * 示意方法,原本已经存在,已经实现的方法
318 | */
319 | public void specificRequest() {
320 | // 具体功能的处理
321 | }
322 | }
323 | ```
324 |
325 | (3) 下面是适配器对象的基本方法。示例代码如下:
326 |
327 | ```java
328 | /**
329 | * 适配器
330 | */
331 | public class Adapter implements Target {
332 | /**
333 | * 持有需要被适配的接口对象
334 | */
335 | private Adaptee adaptee;
336 |
337 | /**
338 | * 构造方法,传入需要被适配的对象
339 | * @param adaptee 需要被适配的对象
340 | */
341 | public Adapter(Adaptee adaptee) {
342 | this.adaptee = adaptee;
343 | }
344 |
345 | public void request() { // ← 这个 request 方法就是用户期望调用的 (Target 接口中的定义)
346 | // 可以转调已经实现了的方法,进行适配
347 | adaptee.specificRequest();
348 | }
349 | }
350 | ```
351 |
352 | (4) 再来看看使用适配器的示例代码如下:
353 |
354 | ```java
355 | /**
356 | * 使用适配器的客户端
357 | */
358 | public class Client {
359 | public static void main(String[] args) {
360 | // 创建需要被适配的对象
361 | Adaptee adaptee = new Adaptee();
362 |
363 | // ↑ 实例化旧的类,也就是需要被适配的对象
364 | // 然后拿给 Adapter 把旧的适配成新
365 | // (客户端期望的,也就是 Target 接口定义的)
366 | //
367 | // 突然想到了浏览器的 polyfill,似乎有点像?
368 |
369 | // 创建客户端期望的接口对象
370 | Target target = new Adapter(adaptee); // ← 把旧的对象拿给 Adapter 处理
371 |
372 | // 请求处理
373 | target.request();
374 | }
375 | }
376 | ```
377 |
378 | 
379 |
380 | ### 使用适配器模式来实现示例
381 |
382 | 使用适配器模式来实现示例,关键是要「实现适配器对象」。
383 |
384 | 它需要第二版的接口,但是在「内部」实现的时候,需要「调用」第一版「已经实现的功能」。
385 |
386 | 也就是说:
387 | - 第二版的「接口」就相当于适配器模式中的「Target 接口」,(注:`class Adapter implements Target`)
388 | - 而第一版已有的实现就「相当于」是适配器模式中的「Adaptee」对象。
389 |
390 | (1) 把适配器简单的实现出来,示意一下。示例代码如下:
391 |
392 | ```java
393 | /**
394 | * 适配器对象,将记录日志「到文件」的功能「适配成」第二版需要的增删改查功能
395 | */
396 | public class Adapter implements LogDbOperateApi {
397 | /**
398 | * 持有需要被适配的对象
399 | */
400 | private LogFileOperateApi adaptee;
401 |
402 | /**
403 | * 构造方法,传入需要被适配的对象
404 | * @param adaptee 需要被适配的对象
405 | */
406 | public Adapter(LogFileOperateApi adaptee) {
407 | this.adaptee = adaptee;
408 | }
409 |
410 | public void createLog(LogModel lm) {
411 | // 1: 先读取文件的内容
412 | List list = adaptee.readLogFile();
413 |
414 | // 2: 加入新的日志对象
415 | list.add(lm);
416 |
417 | // 3: 重新写入文件
418 | adaptee.writeLogFile(list);
419 | }
420 |
421 | public List getAllLog() {
422 | return adaptee.readLogFile();
423 | }
424 |
425 | public void removeLog(LogModel lm) {
426 | // 1: 先读取文件内容
427 | List list = adaptee.readLogFile();
428 |
429 | // 2: 删除相应的日志对象
430 | list.remove(lm);
431 |
432 | // 3: 重新写入文件
433 | adaptee.writeLogFile(list);
434 | }
435 |
436 | public void updateLog(LogModel lm) {
437 | // 1: 先读取文件的内容
438 | List list = adaptee.readLogFile();
439 |
440 | // 2: 修改相应的日志对象
441 | for (int i = 0; i < list.size(); i++) {
442 | if (list.get(i).getLogId().equals(lm.getLogId())) {
443 | list.set(i, lm);
444 | break;
445 | }
446 | }
447 |
448 | // 3: 重新写入文件
449 | adaptee.writeLogFile(list);
450 | }
451 | }
452 | ```
453 |
454 | (2) 此时的「客户端也需要一些改变」。示例代码如下:
455 |
456 | ```java
457 | public class Client {
458 | public static void main(String[] args) {
459 | // 准备日志内容,也就是测试的数据
460 | LogModel lml = new LogModel();
461 | lml.setLogId("001");
462 | lml.setOperateUser("admin");
463 | lml.setOperateTime("2022-03-09 15:22:00");
464 | lml.setLogContent("好耶!");
465 | List list = new ArrayList();
466 | list.add(lml);
467 |
468 | // 创建操作日志文件的对象
469 | LogFileOperateApi logFileApi = new LogFileOperate("");
470 |
471 | // 创建新版操作日志的接口对象
472 | LogDbOperate api = new Adapter(logFileOperateApi);
473 |
474 | // 保存日志文件
475 | api.createLog(lml);
476 |
477 | // 读取日志文件的内容
478 | List allLog = api.getAllLog();
479 | System.out.println("allLog="+allLog);
480 | }
481 | }
482 | ```
483 |
484 | 运行上述代码,测试是否满足要求。
485 |
486 | 评论:
487 |
488 | - 适配器模式,达到的效果是 interface 之间的“无缝”转换,客户端可以使用新的功能,像以前的方式一样调用。
489 | - 使用适配器模式,客户端也需要一些改变。
490 |
491 | [P60] 一些总结的图示
492 |
493 | 转到:[适配器模式 (下)](./3_适配器模式_2.md)
--------------------------------------------------------------------------------
/2_外观模式.md:
--------------------------------------------------------------------------------
1 | # 外观模式 (Facade)
2 |
3 | ## 例子
4 |
5 | 现实生活中,组装计算机,自己买零件组装一台电脑或是买一台已经组装好的整机。不与销售零件的打交道,而是与「装机公司」打交到,给出需求,装机公司直接把组装好的整机返回给我们。
6 |
7 | - 卖配件的公司:模块
8 | - 客户端:为了实现某个功能
9 |
10 | 客户端为了实现某个功能,去自己组合使用模块 or 客户端不用跟系统中的多个模块交互,而且客户端不需要知道那么多模块的细节。
11 |
12 | 实现 Facade 模式。
13 |
14 | ## 代码生成器
15 |
16 | 描述配置的 Model:
17 |
18 | ```java
19 | public class ConfigModel {
20 | /** 是否需要生成表现层,默认是 true */
21 | private boolean needGenPresentation = true;
22 |
23 | /** 是否需要生成逻辑层,默认是 true */
24 | private boolean needGenBusiness = true;
25 |
26 | /** 是否需要生成 DAO,默认是 true */
27 | private boolean needGenDAO = true;
28 |
29 | public boolean isNeedGenPresentation() {
30 | return this.needGenPresentation;
31 | }
32 | public boolean setNeedGenPresentation(boolean needGenPresentation) {
33 | this.needGenPresentation = needGenPresentation;
34 | }
35 |
36 | public boolean isNeedGenBusiness() {
37 | return this.needGenBusiness;
38 | }
39 | public boolean setNeedGenBusiness(boolean needGenBusiness) {
40 | this.needGenBusiness = needGenBusiness;
41 | }
42 |
43 | public boolean isNeedGenDAO() {
44 | return this.needGenDAO;
45 | }
46 | public boolean setNeedGenDAO(boolean needGenDAO) {
47 | this.needGenDAO = needGenDAO;
48 | }
49 | }
50 | ```
51 |
52 | 配置管理:
53 |
54 | ```java
55 | public class ConfigManager {
56 | private static ConfigManager manager = null;
57 | private static ConfigModel cm = null;
58 | private ConfigManager() {
59 | //
60 | }
61 |
62 | public static ConfigManager getInstance() {
63 | if (this.manager == null) {
64 | this.manager = new ConfigManager();
65 | this.cm = new ConfigModel();
66 |
67 | // 读取配置文件,把值设置到 ConfigModel 中去,这里忽略了
68 | }
69 |
70 | return this.manager;
71 | }
72 |
73 | public ConfigModel getConfigData() {
74 | return cm;
75 | }
76 | }
77 | ```
78 |
79 | 配置获取后,按照配置来生成代码:
80 |
81 | ```java
82 | /** 生成表现层模块 */
83 | public class Presentation {
84 | public void generate() {
85 | ConfigModel cm = ConfigManager.getInstance().getConfigData();
86 |
87 | if (cm.isNeedGenPresentation()) {
88 | System.out.println("正在生成表现层代码文件");
89 | }
90 | }
91 | }
92 | ```
93 |
94 | ```java
95 | /** 生成逻辑层模块 */
96 | public class Business {
97 | public void generate() {
98 | ConfigModel cm = ConfigManager.getInstance().getConfigData();
99 |
100 | if (cm.isNeedGenBusiness()) {
101 | System.out.println("正在生成逻辑层代码文件");
102 | }
103 | }
104 | }
105 | ```
106 |
107 | ```java
108 | /** 生成数据层模块 */
109 | public class DAO {
110 | public void generate() {
111 | ConfigModel cm = ConfigManager.getInstance().getConfigData();
112 |
113 | if (cm.isNeedGenDAO()) {
114 | System.out.println("正在生成数据层代码文件");
115 | }
116 | }
117 | }
118 | ```
119 |
120 | 客户端的实现,去掉用多个模块,实现代码文件的生成:
121 |
122 | ```java
123 | public class Client {
124 | public static void main(String[] args) {
125 | // 现在没有配置文件,直接使用默认配置,通常情况下,三层都应该生成
126 | // 也就是说客户端必须对这些模块都有了解,才能够正常地使用它们。
127 | new Presentation().generate();
128 | new Business().generate();
129 | new DAO().generate();
130 | }
131 | }
132 | ```
133 |
134 | 运行结果如下:
135 |
136 | ```
137 | 正在生成表现层代码文件
138 | 正在生成逻辑层代码文件
139 | 正在生成数据层代码文件
140 | ```
141 |
142 | ### 有何问题
143 |
144 | - 客户端需要与生成代码「子系统内部」的多个模块交互
145 | - 对客户端而言,是个麻烦
146 | - 「某个模块发生了改变,客户端也要随着变化」
147 |
148 | for 子系统外部客户端在使用子系统的时候,既能「简单地」使用这些子系统内部的模块功能,而又不用客户端去与子「系统内部」的「多个模块」交互。
149 |
150 | ## 外观模式的定义
151 |
152 | > 为子系统中的「一组接口」提供一个「一致的界面」,Facade 模式定义了一个「高层接口」,这个「接口」使得这一「子系统」更加容易使用。
153 |
154 | ### 界面
155 |
156 | 从一个组件的「外部来看」这个组件,能看到什么,就是这个组件的界面,也就是所谓的「外观」。
157 |
158 | 例如:从一个类的外部来看这个类,public 方法就是这个类的外观。(因为你从这个类的外部看,只能看到这些)
159 |
160 | 再例如:从一个类的外部看这个模块,这个模块对外部的「接口」就是这个模块的外观,因为你只能看到这些接口,其他的模块内部实现的部分都是被接口「封装隔离」了的。
161 |
162 | ### 接口
163 |
164 | 主要指外部和内部「交互的通道」。可以是 interface,但也可以是一个 class 或是 method,并不等价于 (局限于) interface。
165 |
166 | ## 外观模式结构和说明
167 |
168 | 
169 |
170 | 1. Facade
171 |
172 | 定义子系统的多个模块对外的「高层」接口,通常需要调用内部多个模块,从而把客户的请求「代理给」适当的子系统对象。
173 |
174 | 2. 模块
175 |
176 | 接受 Facade 对象的「委派」,真正实现功能,各个模块之间可以有交互。
177 |
178 | 注意:Facade 对象知道各个模块,但模块不应该知道 Facade。
179 |
180 | ## 应用外观模式来解决问题的思路
181 |
182 | 仔细分析上面的问题,客户端想要操作更简单,那就根据客户端的需求来给客户定义一个简单的接口,然后让客户端去调用这个接口,剩下的事情客户端不用管它,这样客户端就变得简单了。
183 |
184 | 当然,这里所说的接口是客户端和被访问的系统之间的一个通道,并不一定是指 Java 的 interface。在它外观模式里面,通常指的是「类」,这个类被称为「外观」。
185 |
186 | 外观模式就是通过「引入这么一个外观类」,在这个类里面「定义客户端想要的简单方法」,然后再这些方法的实现里面,由「外观类再去分别调用内部的多个模块」来实现功能,从而让客户端「变得简单」。
187 |
188 | 这样一来,「客户端就只需要和外观类交互就可以了」。
189 |
190 | 评论:文中多次强调「让客户端变得简单」。客户端与被访问系统之间通过建立一个桥梁 (通道),实现解耦合。让客户端能够不管通道「背后」的东西。
191 |
192 | ## 外观模式示例代码
193 |
194 | - 假设子系统内有三个模块,分别是 AModule, BModule 和 CModule
195 | - 它们分别由一个示意的方法
196 |
197 | ```java
198 | public interface AModuleApi {
199 | public void testA();
200 | }
201 | ```
202 |
203 | 实现 A 模块的接口:
204 |
205 | ```java
206 | public class AModuleImpl implements AModuleApi {
207 | public void testA() {
208 | System.out.println("现在在 A 模块里操作 testA 方法");
209 | }
210 | }
211 | ```
212 |
213 | 同理实现 B 模块:
214 |
215 | ```java
216 | public interface BModuleApi {
217 | public void testB();
218 | }
219 | ```
220 |
221 |
222 | ```java
223 | public class BModuleImpl implements BModuleApi {
224 | public void testB() {
225 | System.out.println("现在在 B 模块里操作 testB 方法");
226 | }
227 | }
228 | ```
229 |
230 | C 同理,然后定义「外观对象」:
231 |
232 | 外观对象就是在「被访问系统」与「客户端」之间建立的「通道」。
233 |
234 | ```java
235 | /**
236 | * 外观对象
237 | */
238 | public class Facade {
239 | /**
240 | * 示意方法,满足客户需要的功能
241 | */
242 | public void test() {
243 | // 在内部实现的时候,可能会调用到内部多个模块
244 | AModuleApi a = new AModuleImpl();
245 | a.testA();
246 | BModuleApi b = new BModuleImpl();
247 | b.testB();
248 | CModuleApi c = new CModuleImpl();
249 | c.testC();
250 | }
251 | }
252 | ```
253 |
254 | 客户端:
255 |
256 | ```java
257 | public class Client {
258 | public static void main(String[] args) {
259 | // 使用 Facade
260 | new Facade().test();
261 | }
262 | }
263 | ```
264 |
265 | 运行结果如下:
266 |
267 | ```
268 | 现在在A模块里面操作testA方法
269 | 现在在B模块里面操作testB方法
270 | 现在在C模块里面操作testC方法
271 | ```
272 |
273 | ## 使用外观模式重写代码生成器
274 |
275 | 新添加一个 Facade 对象:
276 |
277 | ```java
278 | /**
279 | * 代码生成子系统的外观对象
280 | */
281 | public class Facade {
282 | /**
283 | * 客户端需要的,一个简单的调用代码生成的功能
284 | */
285 | public void generate() {
286 | new Presentation().generate();
287 | new Business().generate();
288 | new DAO().generate();
289 | }
290 | }
291 | ```
292 |
293 | 其他定义和实现都没有变化,这里就不再赘述。
294 |
295 | 不再需要客户端去调用子系统内部的多个模块,直接使用外观对象就可以了(变化)。
296 |
297 | 客户端代码:
298 |
299 | ```java
300 | public class Client {
301 | public static void main(String[] args) {
302 | // 使用 Facade
303 | new Facade().generate();
304 | }
305 | }
306 | ```
307 |
308 | 如同上面讲述的例子,Facade 类其实相当于 A、B、C 模块的外观界面,Facade 类也被称为 A、B、C 模块「对外的接口」,有了这个 Facade 类,那么客户端就不需要知道系统内部的实现细节,甚至客户端都不需要知道 A、B、C 模块的存在,客户端只需要跟 Facade 类交互就好了,从而更好地实现了「客户端」和「子系统」中 A、B、C 模块的「解耦」,让客户端更容易地使用系统。
309 |
310 | ## 模式讲解
311 |
312 | ### 外观模式的目的
313 |
314 | - 外观模式不是给子系统添加新的功能接口,而是为了「让外部减少与子系统内多个模块的交互」,「松散解耦」,从而让「外部」能够「更简单地使用子系统」。
315 | - 这一点要特别注意,因为外观是当作子系统对外的接口出现的,虽然也可以在这里定义一些「子系统没有的功能」,但不建议这样做。
316 | - 外观应该是「包装已有的功能」,它主要负责「组合已有功能来实现客户需要,而不是添加新功能」。
317 |
318 | ### 使用外观和不适用外观相比有什么变化
319 |
320 | 有人说:外观模式不就是把原来在客户端的代码搬到 Facade 里面了吗?没什么大变化呀?
321 |
322 | 没错,确实如此,表面上看是把客户端的代码搬到 Facade 里面了,但「实质上」发生了变化。
323 |
324 | - 思考:Facade 到底位于何处,是位于客户端还是由A、B、C模块组成的系统这边?
325 | - 答案:肯定在系统这边,这有什么不一样吗?
326 |
327 | 不一样:如果 Facade 在系统这边,那么就相当于屏蔽了「外部客户端」和「系统内部模块」的交互,从而把 A、B、C 模块组合成为一个「整体」对外,不但「1. 方便了客户端的调用」,而且「2. 封装了系统内部的细节功能」。
328 |
329 | 也就是说,Facade 与各个模块交互的过程已经是内部实现了。这样一来,如果今后调用模块的算法发生了变化,比如变化成要先调用 B,然后调用 A,那么「只需要」修改 Facade 的实现就可以了。
330 |
331 | 另外一个好处:Facade 的功能可以被很多个客户端调用,也就是说 Facade 可以实现「功能的共享」,也就是「实现复用」。同样的调用代码就只用在 Facade 里面写一次就好了,而不用在多个调用的地方重复写。
332 |
333 | 还有一个潜在的好处:对使用 Facade 的人员来说,Facade 大大节省了他们学习成本,他们只需要了解 Facade 即可,无须再深入到子系统内部,去了解每个模块的细节,也不用和多个模块交互,从而使得开发简单,学习也容易。
334 |
335 | ### 有外观可以不使用
336 |
337 | 虽然有了外观,但如果有需要,外部还是可以直接「绕开 Facade」,而直接调用某个具体模块的接口,这样就能实现兼顾组合功能和细节功能。
338 |
339 | 比如在客户端就想要使用 A 模块的功能,那么就不需要使用 Facade,可以直接调用 A 模块的接口。
340 |
341 | 示例代码:
342 |
343 | ```java
344 | public class Client {
345 | public static void main(String[] args) {
346 | AModule a = new AModuleImpl();
347 | a.testA(); // 直接调用
348 | }
349 | }
350 | ```
351 |
352 | ### 外观提供了缺省功能实现
353 |
354 | 很多子系统,客户端不想和子系统多个模块进行交互,因为麻烦和复杂。
355 |
356 | 外观对象可以为用户提供一个简单的、缺省的实现,这个实现对大多数的用户来说都是已经足够了的。但是外观「并不限制」那些「需要更多定制功能」的用户,可以「直接越过外观」去访问内部模块的功能。
357 |
358 | ### 外观模式的实现
359 |
360 | 1. Facade 的实现
361 |
362 | 对于一个子系统而言,外观模式不需要很多,通常可以实现成一个单例。
363 |
364 | 也可以直接把外观中的方法实现成「静态的方法」,这样就可以「不需要创建外观对象的实例」而直接调用,这种实现相当于把「外观类」当成一个「辅助工具」类实现。
365 |
366 | ```java
367 | public class Facade {
368 | private Facade() { }
369 | public static void test() {
370 | AModuleApi a = new AModuleImpl();
371 | a.testA();
372 | BModuleApi b = new BModuleImpl();
373 | b.testB();
374 | CModuleApi c = new CModuleImpl();
375 | c.testC();
376 | }
377 | }
378 | ```
379 |
380 | 2. Facade 可以实现成为 interface
381 |
382 | 虽然 Facade 通常直接实现成为类,但是也可以把 Facade 实现成为真正的 interface。
383 |
384 | 只是这样会增加系统的复杂程度,因为这样会需要一个 Facade 的实现,还需要一个「来获取 Facade 接口对象的工厂」。
385 |
386 | 
387 |
388 | 3. Facade 实现为 interface 的附带好处
389 |
390 | 如果把 Facade 实现成为接口,还附带一个功能,就是能够「有选择性地暴露」接口的方法,尽量减少「模块对子系统外」提供的接口方法。
391 |
392 | > 换句话说,一个模块的「接口中定义」的方法可以分成两部分,一部分是给予系统「外部使用」的,一部分是「子系统内部的模块」间相互调用使用的。有了 Facade 接口,那么用于子系统「内部的接口功能」就「不用暴露给子系统」的「外部」了。
393 |
394 | 「P42 未完待续...」
395 |
396 | 4. Facade 的方法实现
397 |
398 | - Facade 的方法实现中,一般是负责把「客户端的请求」转发给「子系统内部」的各个模块进行处理。
399 | - Facade 的方法实现只是一个功能的「组合调用」。
400 |
401 | 当然,在 Facade 中实现一个逻辑处理也并不是不可以,但「不建议」这样做,因为这「不是 Facade 的本意」,也超出了 Facade 的边界。
402 |
403 | ### 外观模式的优缺点
404 |
405 | 外观模式有如下优点:
406 |
407 | - 松散解耦
408 | 外观模式「松散了」客户端与子系统的「耦合关系」,让子系统内部的模块能够更容易「扩展」和「维护」。
409 | - 简单易用
410 | 外观模式让子系统更加「易用」,客户端「不再需要了解」系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要「跟外观交互」就可以了,相当于外观类为外部客户端使用子系统「提供了一站式服务」。
411 | - 更好地划分访问层次
412 | 通过合理使用 Facade,可以帮助我们更好地「划分访问的层次」。有些方法是对系统外的,有些方法是对系统内部使用的。把需要暴露给系统外部的功能集中到「外观」中,这样既方便客户端使用,也很好地「隐藏了内部的细节」。
413 |
414 | 缺点:
415 |
416 | 过多的或是不太合理的 Facade 也容易让人迷惑。到底是用 Facade 好呢,还是直接调用模块好。(选择困难综合征)
417 |
418 | ## 思考外观模式
419 |
420 | 1. 外观模式的本质
421 |
422 | 封装实现,简化调用。
423 |
424 | Facade 封装了子系统「外部」和子系统「内部」多个模块的交互过程,从而简化了「外部」的调用。通过外观,子系统为外观提供了一些「高层接口」,以便它们的使用。
425 |
426 | 2. 对「设计原则」的体现
427 |
428 | 外观模式很好地体现了「最少知识原则」。
429 |
430 | 如果不使用外观模式,客户端通常需要和子系统内部的「多个模块交互」,也就是说客户端会有「很多的朋友」,客户端和这些模块之间有「依赖关系」,任何一个模块的「变动」都可能会引起客户端的变动。
431 |
432 | 使用外观模式后,客户端「只需要」和外观类交互,也就是说客户端「只有外观这一个朋友」,客户端就不需要去关心子系统内部的变动情况了,客户端「只是和这个外观类有依赖关系」。
433 |
434 | 这样一来,客户端不但简单,而且这个系统会「更有弹性」。当系统内部多个模块发生变化的时候,这个变化可以被这个外观类「吸收和消化」(外观这个朋友,一个人把事情抗着,无论外观那边 (内部) 事情多复杂,多糟糕;反正客户端不用管,外观来承担),并不影响到客户端,换句话说就是:可以在「不影响客户端的情况下,实现系统内部的维护和扩展」。
435 |
436 | 3. 何时选用外观模式
437 |
438 | 建议在以下情况下选择外观模式:
439 |
440 | - 如果希望一个「复杂的子系统」提供一个「简单接口」的时候,可以考虑使用外观模式。使用「外观对象」来实现大部分客户端需要的功能,从而「简化」客户端的使用。
441 | - 如果想要让「客户端程序」和「抽象类」的实现部分「松散解耦」,可以考虑使用外观模式,使用外观对象来将这个系统与它的客户端分离开来,从而提高子系统的「独立性」和「可移植性」。
442 | - 如果构建「多层结构」的系统,可以考虑使用外观模式,使用外观对象作为每层的入口,这样可以「简化层间调用」,也可以「松散」层次之间的「依赖关系」。
443 |
444 | ## 相关模式
445 |
446 | ### 外观模式和中介者模式
447 |
448 | 这两个模式非常类似,但却有本质的区别。
449 |
450 | - **中介者模式主要用来封装多个对象之间相互的交互**,多用于在系统内部的多个模块之间;而外观模式封装的是单向的交互,是从客户端访问系统的调用,没用从系统中访问客户端的调用。(一个是双向,一个是单向交互)。
451 | - **在中介者模式的实现里面**,是需要实现具体的交互功能的;而外观模式的实现里面,一般是组合调用或是转调内部实现的功能,通常外观模式本身并不实现这些功能。(一个需要主动实现交互功能,一个是被动的,被动根据已实现的模块功能进行 Facade 类的编写)
452 | - **中介者模式的目的主要是松散多个模块之间的解耦**,把这些耦合关系全部放到中介者中去实现;而外观模式的目的是简化客户端的调用,这点和中介者模式也不同。(一个是将耦合关系交给中间人、中介者来做,一个是简化客户端的调用)
453 |
454 | ### 外观模式和单例模式
455 |
456 | - 通常一个子系统只需要一个外观实例,所以外观模式也可以和单例模式「组合使用」,把 Facade **类实现成为单例**。当然,也可以跟前面示例的那样,把外观模类构造方法私有化,然后把提供给客户端的方法实现成为静态的。(两者组合使用)
457 |
458 | ### 外观模式和抽象工厂模式
459 |
460 | 外观模式的外观通常需要和系统内部的多个模块交互,每个模块一般都有自己的接口,**所以在外观类的实现里面**,需要获取这些接口,然后组合这些接口来完成客户端的功能。`
461 |
462 | 那么怎么获取这些接口呢?就可以和抽象工厂一起使用,外观类通过抽象工厂来获取所需要的接口,而抽象工厂也可以把模块内部的实现对 Facade 进行屏蔽,也就是说 Facade 也仅仅只是知道它从模块中获取它需要的功能,模块内部的细节,Facade 也不知道。
463 |
--------------------------------------------------------------------------------
/5_工厂方法模式.md:
--------------------------------------------------------------------------------
1 | # 工厂方法模式 (Factory Method)
2 |
3 | ## 场景问题
4 |
5 | ### 导出数据的应用框架
6 |
7 | 考虑到这样一个实际应用:实现一个「导出数据的应用框架」,让客户「选择数据的导出方式」,并「正真执行数据导出」。
8 |
9 | 在一些实际的企业应用中,一个公司的系统往往分散在很多个不同的地方运行,比如各个分公司或者是门市点。公司「既没有建立全公司专有网络的实力」,但又「不愿意让业务数据实时地在广域网上传递」,一个是考虑数据安全的问题,另一个是运行速度的问题。
10 |
11 | 这种系统通常会又一个「折中的方案」,那就是各个分公司内运行系统的时候是「独立的」,是在自己分公司的「局域网内运行」。「每天业务结束的时候」,各个分公司会导出自己的业务数据,然后把业务数据打包,通过网络传输给总公司,或是专人把数据送到总公司,然后由总公司进行数据导入和核算。
12 |
13 | 通常这种系统在导出数据上会有一些「约定的方式」,比如导出成「文本格式、数据库备份形式、Excel 格式、XML 格式」等。
14 |
15 | 现在就来考虑实现这样一个应用框架。在继续之前,先来了解一些关于框架的知识。
16 |
17 | ### 框架的基础知识
18 |
19 | #### 1. 框架是什么
20 |
21 | 简单点说,**框架就是能完成一定功能的「半成品软件」。** (完成一半任务,但不全部完成)
22 |
23 | 就其本质而言,框架是一个软件,而且是一个半成品软件。所谓半成品,就是「还不能完全实现」用户需要的功能。框架知识实现用户需要功能的「一部分」,还需要「进一步加工」,才能成为一个「满足用户需要的」、「完整的软件」。因此框架级的软件,它的「主要客户是开发人员,而不是最终用户」。
24 |
25 | 延伸:有些朋友会想,既然框架只是个半成品,那么何必要去学习和使用框架呢,学习成本也不算小?那就是因为框架能完成一定的功能,也就是 "框架已经完成的一定的功能" 在吸引着开发者,让大家去学习和使用框架。
26 |
27 | #### 2. 框架能干什么
28 |
29 | **能完成一定功能,加快应用开发速度。**
30 |
31 | 由于框架完成了一定的功能,而且通常是一些「基础的、有难度的、通用的」功能,这就「避免」我们在开发应用的时候「完全从头开始」,而是在框架已有的功能上继续开发,也就是说「会复用框架的功能」,从而「加快应用的开发进度」。
32 |
33 | **给我们一个精良的程序架构。**
34 |
35 | 框架定义了应用的整体架构,包括「类和对象的分割」、「各部分的主要责任」、「类和对象怎么协作」,以及「控制流程」等。
36 |
37 | Java 界流行的框架,大多出自「大师手笔」,「设计都很精良」。基于这样的框架来开发,一般会「遵循框架已经规划好的结构来进行开发」,从而使开发应用程序的「结构也相对变得精良了」。
38 |
39 | 评论:
40 |
41 | - 框架完成一半的任务,但不全部完成。
42 | - 开发者 → 框架 (半成品) -加工→ 最终用户
43 | - 框架给我们提供一个固定的规范,开发者之间也能通过框架建立起沟通的桥梁。
44 | - 框架提供一个完备的生命周期。
45 | - 相当于大师引导你对代码进行布局,模块进行分割。
46 |
47 | #### 3. 对框架的理解
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 | #### 4. 框架和设计模式的关系
82 |
83 | **(1) 设计模式比框架「更抽象」**
84 |
85 | 框架已经是实现出来的软件了,虽然只是个「半成品的软件」,但毕竟是已经实现出来的了;而「设计模式」的「重心」还在于「**解决问题的方案上**」,也就是还「**停留在思想的层面上**」。因此设计模式比框架更为抽象。
86 |
87 | **(2) 设计模式是比框架「更小的体系结构元素」**
88 |
89 | 如上所述,框架是已经实现出来的软件,并「实现了一系列的功能」,因此「一个框架通常会包含多个设计模式的应用」。
90 |
91 | **(3) 框架比设计模式「更加特例化」**
92 |
93 | 框架是完成一定功能的「半成品软件」,也就是说,框架的目的很明确,就是要「解决某一个领域的某些问题」,那是具体的功能。「不同的领域实现出来的框架是不一样的」。
94 |
95 | 而设计模式还停留在「思想层面」,只要相应的问题适合用某个设计模式来解决,在不同的领域都可以应用。
96 |
97 | 因此,框架「总是针对特定领域的」,而设计模式「更加注重从思想上、方法上」来解决问题,「更加通用化」。
98 |
99 | 评论:
100 |
101 | - 用「设计模式」来设计「框架」
102 | - 很多特定的情况,运用不同的模式看哪个适合,特殊问题特殊考虑。换言之,某些东西只适合某些特定的设计,这样设计比较好用。就像前端、后端设计、MVC、MVVM、BS、SaaS、O2O、P2P 哈哈。
103 |
104 | ### 有何问题
105 |
106 | 分析上面要实现的应用框架,「不管用户选择什么样的导出格式」,最后「导出的都是一个文件」,而且「系统并不知道」究竟要导出成为「什么样的文件」,因此应该有一个「**统一的接口**」来「描述系统最后生成的对象」,并「操作输出的文件」。
107 |
108 | 先把导出的文件对象的接口定义出来。实例代码如下:
109 |
110 | ```java
111 | /**
112 | * 导出的文件对象的接口
113 | */
114 | public interface ExportFileApi {
115 | /**
116 | * 导出内容成为文件
117 | * @param data 示意:需要保存的数据
118 | * @return 是否导出成功
119 | */
120 | public boolean export(String data);
121 | }
122 | ```
123 |
124 | 对于实现导出数据的「业务功能对象」,它应该「根据需要」来创建相应的 ExportFileApi 的实现对象,因为特定的 ExportFileApi 的「实现」是与具体的业务相关的。但是对于「实现导出数据的业务功能对象」而言,它「并不知道应该创建哪一个」ExportFileApi 的实例对象,「也不知道应该如何创建」。
125 |
126 | 也就是说:对于「实现」导出数据的业务功能对象,它需要「创建」ExportFileApi 的「具体实例对象」,但是它「只知道 ExportFileApi 接口」,而「不知道其具体的实现」,那该怎么办呢?
127 |
128 | ## 解决方案
129 |
130 | ### 使用工厂方法模式来解决问题
131 |
132 | 用来解决上述问题的一个合理的解决方案就是「工厂方法模式 (Factory Method)」。那么什么是工厂方法模式呢?
133 |
134 | #### 1. 工厂方法模式的定义
135 |
136 | > 定义一个「用于创建对象的接口」,让「子类决定」实例化哪一个类,「Factory Method」使一个类的实例化「延迟到」其「子类」。
137 |
138 | 注意:延迟执行的思想,延迟到「其子类」。
139 |
140 | #### 2. 应用工厂模式来解决问题的思路
141 |
142 | 仔细分析上面的问题,事实上「在实现导出数据的业务功能对象里面」,根本就「不知道究竟要使用哪一种」导出文件的格式,因此这个对象根本就「不应该」和「具体的导出文件的对象」「耦合在一起」,它只需要「面向导出的文件对象接口」就可以了。
143 |
144 | 评论:为了实现解耦合,中间插入一层,这一层可以塞很多相同的 interface (Product) 的「实现」。
145 |
146 | 这不就自相矛盾了吗?「要求面向接口,不让和具体的实现耦合」,但「又需要创建接口的“具体实现对象”的实例」。这么解决这个矛盾啦?
147 |
148 | **工厂方法模式的解决思路很有意思,那就是「不解决」,采用「无为而治」的方式**;不是需要「接口对象」吗,那就「定义一个方法」来创建;可是事实上它「自己是不知道如何创建」这个接口对象的 (自己不知道,但用子类创建),没有关系,定义成「抽象**方法**」就可以了,自己实现不了,那就让「子类」来实现,这样「这个对象本身」就可以是「面向接口编程」,而「无需关心到底如何创建接口对象」了。
149 |
150 |
151 | ### 工厂方法模式的结构和说明
152 |
153 | 
154 |
155 | 
156 |
157 | - Product: 定义工厂方法「所创建的对象」的接口,也就是实际「需要使用的对象」的接口。
158 | - ConcreteProduct: 具体的 Product 接口的「实现对象」。
159 | - Creator: 「创造器」,声明工厂方法,工厂「方法」通常会「返回一个 Product 类型的实例对象」,而且「多是抽象方法」。也可以在 Creator 里面提供工厂方法的「默认实现」,让工厂方法「返回一个缺省的 Product 类型的实例对象」。
160 | - ConcreteCreator: 具体的「创建器对象」,覆盖实现 Creator 定义的工厂方法,返回「具体的 Product 实例」。
161 |
162 | ### 工厂方法模式示例代码
163 |
164 | **(1) Product 定义的示例代码如下:**
165 |
166 | ```java
167 | /**
168 | * 工厂方法所创建的对象的接口
169 | */
170 | public interface Product {
171 | // 可以定义 Product 的属性和方法
172 | }
173 | ```
174 |
175 | **(2) Product 实现对象的示例代码如下:**
176 |
177 | ```java
178 | /**
179 | * 具体的 Product 对象
180 | */
181 | public class ConcreteProduct implements Product {
182 | // 实现 Product 要求的方法
183 | }
184 | ```
185 |
186 | **(3) 创建器定义的示例代码如下:**
187 |
188 | ```java
189 | /**
190 | * 创建器 声明工厂方法
191 | */
192 | public abstract class Creator {
193 | /**
194 | * 创建 Product 的工厂方法
195 | * @return Product 对象
196 | */
197 | protected abstract Product factoryMethod();
198 |
199 | /**
200 | * 示意方法,实现某些功能的方法
201 | */
202 | public void someOperation() {
203 | // 通常在这些方法实现中需要调用工厂方法来获取 Product 对象
204 | Product product = factoryMethod();
205 | }
206 | }
207 | ```
208 |
209 | **(4) 创建器实现对象的示例代码如下:**
210 |
211 | ```java
212 | /**
213 | * 具体的创建器实现对象
214 | */
215 | public class ConcreteCreator extends Creator {
216 | protected Product factoryMethod() {
217 | // 重新定义工厂方法,返回一个具体的 Product 对象
218 | return new ConcreteProduct();
219 | }
220 | }
221 | ```
222 |
223 | ### 使用工厂方法模式来实现示例
224 |
225 | 要使用工厂方法来实现示例,先来按照工厂方法模式的结构,对应出「哪些是被创建的 Product,哪些是 Creator」。
226 |
227 | 分析要求实现的功能,「导出的文件对象接口 ExportFileApi」就相当于是「Product」,而「用来实现导出数据的业务功能对象」就相当于「Creator」。
228 |
229 | 把 Product 和 Creator「**分开**」后,就可以分别来实现它们了。
230 |
231 | 使用工厂模式来实现示例程序结构如图:
232 |
233 | 【待补充】[P106]
234 |
235 | 下面一起来看看代码实现。
236 |
237 | (1) 导出的文件对象接口 ExportFileApi 的实现没有变化,这就不再赘述了。
238 |
239 | (2) 接口 ExportFileAPi 的实现。为了示例简单,只实现导出「文本文件」格
240 | 式和「数据库备份文件」两种。
241 |
242 | 实现导出「文本文件格式」示例代码如下:
243 |
244 | ```java
245 | /**
246 | * 导出成文本文件格式的对象
247 | */
248 | public class ExportTxtFile implements ExportFileApi {
249 | public boolean export(String data) {
250 | // 简单示意一下,这里需要操作文件
251 | System.out.println("导出数据"+data+"到文本文件");
252 | return true;
253 | }
254 | }
255 | ```
256 |
257 | 导出成「数据库备份文件」形式对象的实例代码如下:
258 |
259 | ```java
260 | /**
261 | * 导出成数据库备份文件形式的对象
262 | */
263 | public class ExportDb implements ExportFileApi {
264 | public boolean export(String data) {
265 | // 简单示意一下,这里需要操作数据库和文件
266 | System.out.println("导出数据"+data+"到数据库备份文件");
267 | return true;
268 | }
269 | }
270 | ```
271 |
272 | (3) 实现 ExportOperate 的示例代码如下:
273 |
274 | ```java
275 | /**
276 | * 实现导出数据的业务功能对象
277 | */
278 | public abstract class ExportOperate {
279 | /**
280 | * 导出文件
281 | * @param data 需要保存的数据
282 | * @return 是否成功导出文件
283 | */
284 | public boolean export(String data) {
285 | // 使用工厂方法
286 | ExportFileApi api = factoryMethod();
287 | return api.export(data);
288 | }
289 |
290 | /**
291 | * 工厂方法,创建导出的文件对象的「接口对象」
292 | * @return 导出文件对象的接口对象
293 | */
294 | protected abstract ExportFileApi factoryMethod();
295 | }
296 | ```
297 |
298 | (4) 加入了两个 Creator 实现。
299 |
300 | 创建导出成文本文件格式对象的示例代码如下:
301 |
302 | ```java
303 | /**
304 | * 具体的创建器实现对象,实现「创建导出成文本格式的对象」
305 | */
306 | public class ExportTxtOperate extends ExportOperate {
307 | protected ExportFileApi factoryMethod() {
308 | // 创建导出成文本文件格式的对象
309 | return new ExportTxtFile();
310 | }
311 | }
312 | ```
313 |
314 | 创建导出成数据库备份文件形式对象的示例代码如下:
315 |
316 | ```java
317 | /**
318 | * 具体的创建器实现对象,实现「创建导出成数据库备份文件形式的对象」
319 | */
320 | public class ExportDBOperate extends ExportOperate {
321 | protected ExportFileApi factoryMethod() {
322 | // 创建导出成数据库备份文件形式的对象
323 | }
324 | }
325 | ```
326 |
327 | (5) 客户端直接创建「需要使用的 Creator 对象」,然后「调用相应的功能方法」。示例代码如下:
328 |
329 | ```java
330 | public class Client {
331 | public static void main(String[] args) {
332 | // 创建需要使用的 Creator 对象
333 | ExportOperate operate = new ExportDBOperate(); // ← DB/File 只是 new 的不一样
334 |
335 | // 调用输出数据库的功能方法
336 | operate.export("测试数据"); // ← 但调用的方法是一样的 (return 的 Type 是一样的)
337 | }
338 | }
339 | ```
340 |
341 | 运行结果如下:
342 |
343 | ```
344 | 导出数据测试数据到数据库备份文件
345 | ```
346 |
347 | 还可以「修改客户端 new 的对象」,切换成其他实现对象,试试会发生什么。看来应用工厂方法模式是很简单的,对吧。
348 |
349 | ## 模式讲解
350 |
351 | ### 认识工厂方法模式
352 |
353 | #### 1. 工厂方法模式的功能
354 |
355 | 工厂方法模式主要功能是「让父类在不知道具体实现」的情况下,完成「自身的功能调用」;而具体的实现「延迟到子类」来实现。
356 |
357 | 这样在设计的时候,「不用考虑具体的实现」,需要某个对象,把它「通过工厂方法返回」就好了,在「使用这些对象实现功能的时候」还是「**通过接口来操作**」,这类似与「**IoC/DI**」的思想,这个在后面讲给大家稍详细点介绍。
358 |
359 | #### 2. 实现抽象类
360 |
361 | 工厂方法的实现中,通常「父类」会是一个「抽象类」,里面「包含创建所需对象的抽象方法」,这些「抽象方法」就是「工厂方法」。
362 |
363 | 注意:这里要注意一个问题,子类在实现这些抽象方法的时候,通常「并不是正**真正的**由子类来实现**具体的功能**」,而是「在子类的方法里里面**做选择**」,选择「具体的产品实现对象」。
364 |
365 | 父类里面,通常会有「使用这些产品」来「实现一定功能的方法」,而且这些方法所实现的功能通常是「公共」的功能,「不管子类选择了何种具体的产品实现,这些方法的功能都总是能正常执行」。
366 |
367 | #### 3. 实现成具体的类
368 |
369 | 也可以把父类「实现成为一个具体的类」,这种情况下,通常是「在父类中提供获取所需的对象**默认实现方法**」,这样「即使没有具体的子类,也能够运行」。(加个默认方法)
370 |
371 | 通常这种情况「还是需要具体的**子类来决定**」「具体要如何创建**父类所需要的对象**」。也把这种情况称为「工厂方法为子类提供了**挂钩**」。通过工厂方法,可以让子类对象来「覆盖」父类的实现,从而「提供更好的灵活性」。
372 |
373 | #### 4. 工厂方法的参数和返回
374 |
375 | 工厂方法的实现中,「可能需要参数」,以便「决定到底选用哪一种具体的实现」。也就是说「通过在抽象方法里面**传递参数**」,在「子类实现」的时候「根据参数进行选择」,看看究竟应该「创建哪一个**具体的实现对象**」。
376 |
377 | 一般工厂方法返回的是「被创建对象的接口对象」,当然「也可以是**抽象类**或者**一个具体的类的实例**」(不局限于接口对象)。「标星」
378 |
379 | #### 5. 谁来使用工厂方法创建的对象
380 |
381 | 这里首先要弄明白一件事情,就是谁在使用「工厂方法创建的对象」?
382 |
383 | 事实上,在工厂方法模式里面,应该是 **Creator 中的「其它方法」在使用「工厂方法创建的对象」**,这个时候「工厂方法创建的对象」,是 Creator 中的某些方法使用;
384 |
385 | 对于使用那些由 Creator 创建出来的对象,这个时候「工厂方法创建的对象」,是「构成客户端需要的对象的**一部分**」。分别举例来说明。
386 |
387 | **1) 客户端使用 Creator 对象的情况**
388 |
389 | 比如前面的示例,对于「实现导出数据业务功能的对象」的类 ExportOperate,它有一个 export 的方法,在这个方法里面,需要使用具体的「导出的文件对象的接口对象 ExportFileApi」,而 ExportOperate 是不知道具体的 ExportFileApi 实现的,那是怎么做的呢?就是定义了一个「工厂方法」,用来返回 ExportFileApi 的对象,然后 export 方法会使用这个「工厂方法」来获取它「所需要的对象」,然后执行功能。
390 |
391 | 这个时候的客户端是怎么做的?这个时候客户端主要是使用 ExportOperate 的实例来完成它想要完成的功能,也就是客户端使用 Creator 对象的情况。简单描述这种情况下的代码结构如下:
392 |
393 | ```java
394 | /**
395 | * 客户端使用 Creator 对象的情况下,Creator 的基本实现结构
396 | */
397 | public abstract class Creator {
398 | /**
399 | * 工厂方法,一般不对外
400 | * @return 创建的产品对象
401 | */
402 | protected abstract Product factoryMethod();
403 |
404 | /**
405 | * 提供给外部使用的方法
406 | * 客户端一般使用 Creator 提供的这些方法来完成所需要的功能
407 | */
408 | public void someOperation() {
409 | // 在这里使用工厂方法
410 | Product p = factoryMethod();
411 | }
412 | }
413 | ```
414 |
415 | **2) 客户端使用由 Creator 创建出来的对象**
416 |
417 | 另外一种是由 Creator 向客户端返回「由工厂方法创建的对象」来构建的对象,这个时候工厂方法创建的对象,是「构成客户端需要的对象的一部分」。简单描述这种情况下的代码结构如下:
418 |
419 | ```java
420 | /**
421 | * 客户端使用 Creator 来创建客户端需要的对象的情况下,Creator 的基本实现结构
422 | */
423 | public abstract class Creator {
424 | /**
425 | * 工厂方法,一般不对外,创建一个部件对象
426 | * @return 创建的产品对象,一般是另一个产品对象的部件
427 | */
428 | protected abstract Product1 factoryMethod1();
429 |
430 | /**
431 | * 工厂方法,一般不对外,创建一个部件对象
432 | * @return 创建的产品对象,一般是另一个产品对象的部件
433 | */
434 | protected abstract Product2 factoryMethod();
435 |
436 | /**
437 | * 创建客户端需要的对象,客户端主要使用产品对象来完成所需要的功能
438 | * @return 客户端需要的对象
439 | */
440 | public Product createProduct() {
441 | // 在这里使用工厂方法,得到客户端所需对象的部件对象
442 | Product1 p1 = factoryMethod1();
443 | Product2 p2 = factoryMethod2();
444 |
445 | // 工厂方法创建的对象是创建客户端对象所需要的
446 | Product p = new ConcreteProduct();
447 | p.setProduct1(p1); // ← 这里用到注入的思想了
448 | p.setProduct2(p2);
449 |
450 | return p;
451 | }
452 | }
453 | ```
454 |
455 | 小结一下:在工厂方法模式里,客户端「要么使用 Creator 对象,要么使用 Creator 创建的对象」,「一般客户端不直接使用工厂方法」。当然也可以直接把工厂方法暴露给客户端操作,但是一般不这么做。
456 |
457 | #### 工厂方法模式的调用顺序示意图
458 |
459 | 由于客户端使用 Creator 对象有两种典型情况,因此调用的顺序示意图也分为两种情况。
460 |
461 | 先看看客户端「使用 Creator 创建出来的对象」情况的调用顺序示意图,如图:
462 |
463 | 
464 |
465 | 接下来看看客户端「使用 Creator 对象」时候的调用顺序示意图,如图:
466 |
467 | 
468 |
469 |
470 | 转到:[工厂方法模式 (下)](./5_工厂方法模式_2.md)
471 |
--------------------------------------------------------------------------------
/5_工厂方法模式_2.md:
--------------------------------------------------------------------------------
1 | ### 工厂方法模式与 IoC/DI
2 |
3 | - IoC —— Inversion of Control,控制反转。
4 | - DI —— Dependency Injection,依赖注入。
5 |
6 | #### 1. 如何理解 IoC/DI
7 |
8 | 想要理解上面两个概念,就必须搞清楚如下的问题:
9 |
10 | - 参与者都有谁?
11 | - 依赖:谁依赖于谁?为什么需要依赖?
12 | - 注入:谁注入于谁?到底注入什么?
13 | - 控制反转:谁控制谁?控制什么?为什么叫反转 (有反转就应该有正转了)?
14 | - 依赖注入和控制反转是同一概念吗?
15 |
16 | 下面就来简单地回答一下上述问题,把这些问题搞明白了,也就明白 IoC/DI 了。
17 |
18 | - (1) 参与者都有谁:一般有「三方参与者」,一个是「某个对象」;另一个是「IoC/DI」的容器;还有一个是「某个对象的外部资源」。
19 |
20 | 解释:解释一下名词,某个对象指的就是任意的、普通的 Java 对象,IoC/DI 的「容器」简单点说就是指用来实现 IoC/DI 功能的一个「框架程序」;对象的「外部资源」就是「对象需要的」,但是「是从对象外部获取的」,都「统称为资源」,比如,对象需要的其他对象,或者是对象「需要的文件资源」等。
21 |
22 | - (2) 谁依赖于谁:当然是某个「对象」依赖于 IoC/DI 的容器。
23 | - (3) 为什么需要依赖:对象「需要 IoC/DI 的容器」来「提供对象的外部资源」。
24 | - (4) 谁注入于谁:很明显是 IoC/DI 的容器注入某个对象。
25 | - (5) 到底注入什么:就是注入某个对象「所需要的外部资源」。
26 | - (6) 谁控制谁:当然是「IoC/DI 的容器」来控制对象了。
27 | - (7) 控制什么:主要是控制对象实例的「创建」。
28 | - (8) **为何叫反转**:反转是「相对于正向」而言的,那么什么算是正向呢?考虑一下常规情况下的应用程序,如果要「在 A 里面使用 C」,你会怎么做呢?当然是直接去创建 C 的对象,也就是说,在 A 类中「主动」去获取所需要的外部资源 C,这种情况「被称为正向的」。那么什么是「反转」呢?就是在 A 类「不再主动」去获取 C,而是「被动等待」,「等待 IoC/DI 的容器」获取一个 C 的实例,然后「反向地注入」到 A 类中。
29 |
30 | 用图例来说明一下。
31 |
32 | 先看没有 IoC/DI 的时候,常规的 A 类使用 C 的示意图,如图所示:
33 |
34 | 
35 |
36 | 当有了 IoC/DI 容器后,A 类「不再主动去创建 C 了」,如图:
37 |
38 | 
39 |
40 | 而是「被动等待」,等待 IoC/DI 的容器获取一个 C 的实例,然后「反向地注入到 A 类中」,如图:
41 |
42 | 
43 |
44 | - (9) 依赖注入和控制反转是同一概念吗?
45 |
46 | 根据上面的讲述,应该能看出来,依赖注入和控制反转是「对同一件事情的不同描述」。从某个方面讲,就是它们「描述的角度不同」。依赖注入是从「应用程序角度」去描述的,可以把依赖注入描述得完整点:**应用程序依赖容器创建注入它所需要的外部资源**;而控制反转是从「容器的角度」去描述,描述得完整点就是:**容器控制应用程序,由容器「反向地」向应用程序「注入」其所需要的外部资源**。
47 |
48 | 小结:其实 IoC/DI 对编程带来的最大改变「不是在代码上,而是在思想上」,发生了「主从换位」的变化。应用程序「原本是老大」,要获取什么资源都是「主动出击」,现在 IoC/DI 的思想中,应用程序就变得被动了,被动地等待 IoC/DI 容器来创建并注入它需要的资源了。
49 |
50 | 这么小小的一个改变其实是「编程思想的一个大进步」,这样就「有效地分离了**对象**和**它所需要的外部资源**」,使得它们「松散解耦」,有利于「功能复用」,更重要的是使得整个程序的「整个体系结构变得非常灵活」。
51 |
52 | 评论:依赖注入和控制反转是对一件事情的不同描述,描述的角度不同,前者应用程序角度,后者容器的角度;这种思想让对象与其需要的外部资源之间「松散解耦」,使得整个体系更加灵活;例如一个模块,由于松散解耦了,所以可以单独拿出来使用,方便集成测试之类的。
53 |
54 | #### 2. 工厂方法模式和 IoC/DI 的关系
55 |
56 | 从某个角度讲,工厂方法模式和 IoC/DI 的思想很类似。
57 |
58 | 评论:多出来个第三者 (例如:IoC/DI 容器),把某些事情交给第三者来做,实现原来两者之间的「松散解耦」,让整个体系结构变得灵活,两者不再像原来一样紧密联系。
59 |
60 | 上面讲了,有了 IoC/DI 后,应用程序就「不再主动」了,而是「被动地等待」由容器来注入资源。那么在编写代码的时候,一旦要用到外部资源,就会「开一个窗口」,让容器能注入进来,也就是「提供给容器一个注入的途径」,当然这不是我们的重点,就不去细细讲解了,用 setter 注入来示例一下,使用 IoC/DI 的示例代码如下:
61 |
62 | ```java
63 | public class A {
64 | /**
65 | * 等待被注入进来
66 | */
67 | private C c = null;
68 |
69 | /**
70 | * 注入资源 C 的方法
71 | * @param c 被注入的资源
72 | */
73 | public void setC(C c) {
74 | this.c = c;
75 | }
76 |
77 | public void t1() {
78 | // 这里需要使用 C,可是又不让主动去创建 C 了,怎么办?
79 | // 反正就要求「从外部注入」,这样更省心
80 | // 自己不用管怎么获取 C,直接使用就好了
81 | c.tc();
82 | }
83 | }
84 | ```
85 |
86 | 接口 C 的示例代码如下:
87 |
88 | ```java
89 | public interface C {
90 | public void tc();
91 | }
92 | ```
93 |
94 | 从上面的示例代码可以看出,现在 A 里面写代码的时候,凡是碰到了需要外部资源,那么就「提供注入的途径」,要求「从外部注入」,自己「只管使用这些对象」。
95 |
96 | 再来看看「工厂方法模式」,如何实现上面同样的功能。为了区分,分别取名为 A1 和 C1。这个时候在 A1 里面要使用 C1 对象,也不是由 A1 主动去获取 C1 对象,而是「创建一个工厂方法」,类似于「一个注入的途径」;然后由子类,假设叫 A2 吧,由 A2 来获取 C1 对象,在调用的时候,替换掉 A1 的相应方法,相当于「反向注入回到 A1 里面」。示例代码如下:
97 |
98 | ```java
99 | public abstract class A1 {
100 | /**
101 | * 工厂方法,创建 C1,类似于从子类注入进来的途径
102 | * @return C1 的对象实例
103 | */
104 | protected abstract C1 createC1();
105 |
106 | public void t1() {
107 | // 这里需要使用 C1 类,可是不知道究竟是用哪一个
108 | // 也就不主动去创建 C1 了,怎么办?
109 | // 反正会在子类里面,这里不用管怎么获取 C1,直接使用就好了
110 | createC1().to();
111 | }
112 | }
113 | ```
114 |
115 | 子类示例代码如下:
116 |
117 | ```java
118 | public class A2 extends A1 {
119 | protected C1 createC1() {
120 | // 真正的选择具体的实现,并创建对象
121 | return new C2();
122 | }
123 | }
124 | ```
125 |
126 | C1 接口和前面的 C 接口是一样的,C2 这个实现类也是空的,只是演示一下,因此就不去展示它们的代码了。
127 |
128 | 提示:仔细体会上面的示例,对比它们的实现,尤其是从思想层面上,会发现工厂方法模式和 IoC/DI 的思想是相似的,都是「主动变被动」,进行了「主从换位」,从而获得了「更灵活」的程序结构。
129 |
130 | 评论:工厂方法模式和 IoC/DI 的思想类似,工厂方法父类提供一个 abstract createC1 方法,子类去实现这个 createC1 方法,也就相当于:父类提供了一个「注入的途径 / 窗口」;子类向父类「反向注入」创建后的对象,父类只管使用它,这样就实现了「主从换位」,获得更灵活的程序结构。父类不管你子类怎么创建,反正只要东西是我想要的就行,子类这边就可以使用「各种方法」创建父类需要的东西,十分灵活。
131 |
132 | ### 平行的类层次结构
133 |
134 | #### 1. 平行的类层次结构的含义
135 |
136 | 简单点说,假如有「两个」类层次结构,「**其中一个类层次中的每个类**在**另一个类层次中**都有**一个对应的类的结构**」,就被称为「平行的**类**层次结构」。
137 |
138 | 举个例子来说,硬盘对象有很多种,如分成「台式机硬盘」和「笔记本硬盘」,在台式机硬盘的「具体实现」上面,又有「希捷、西数等」不同的品牌的实现,同样在笔记本硬盘上,也有「希捷、日立、IBM 等」不同品牌的实现;硬盘对象「具有自己的行为」,如硬盘能存储数据,也能从硬盘上获取数据,不同的硬盘对象对应的「行为对象是不一样的」,因为不同的硬盘对象,它的「行为的实现方式」是不一样的。如果把「硬盘对象」和「硬盘对象的行为」分开描述,那么就构成了如图所示的结构:
139 |
140 | 
141 |
142 | 「硬盘对象」是一个类层次,「硬盘的行为」也是一个类层次,而且「两个层次中的类」是「对应的」。台式机希捷「硬盘对象」就对应着「硬盘行为」里面的台式机希捷硬盘的行为;笔记本 IBM 硬盘就对应着笔记本 IBM 硬盘的行为,这就是一种「典型的平行的类层次结构」。
143 |
144 | 「**标星**」这种平行的类层次结构用来干什么?主要用来:把一个层次中的「某些行为」分离出来,让层次中的类把「原本属于自己的责任」,「委托给分离出来的类」去实现,从而使得「类层次本身变得简单」,「更容易扩展和复用」。
145 |
146 | 一般来讲,分离出去的这些类的「行为」,会「对应着」类层次结构来组织,从而「形成一个新的类层次结构」,相当于原来对象的类层次结构,而这个层次结构和原来的类层次结构是「存在对应关系的」,因此被称为「平行的类层次结构」。
147 |
148 | #### 2. 工厂方法模式和平行的类层次结构的关系
149 |
150 | 可以使用「工厂方法模式」来「连接平行的类层次」。
151 |
152 | 如刚刚的图所示,在每个硬盘对象里面,都有一个「工厂方法」createHDOperate,通过这个「工厂方法」,客户端可以获取一个和「硬盘对象相对应的行为对象」。在硬盘对象的「子类」里面,会「覆盖父类的工厂方法 createHDOperate」,以「提供与**自身对应**的行为对象」,从而「自然地」把两个平行的类层次「连接起来使用」。
153 |
154 | ### 参数化工厂方法
155 |
156 | 所谓的参数化工厂方法指的就是:通过给工厂方法「传递参数」,让工厂方法根据参数的不同来「创建不同的产品对象」,这种情况就被称为「参数化工厂方法」。当然工厂方法创建的「不同产品」必须是「同一个 Product 类型」。
157 |
158 | 来改造前面的示例,现在「由一个工厂方法」来「创建 ExportFileApi 这个产品对象」,但是 ExportFileApi 接口的实现很多,为了「方便创建的选择」,直接「从客户端传入一个参数」,这样在需要创建 ExportFileApi 对象的时候,就把这个「参数传递给」工厂方法,让工厂方法来实例化具体的 ExportFileApi 实现对象。
159 |
160 | 还是看看代码示例会比较清楚。
161 |
162 | (1) 先来看看 Product 接口,就是 ExportFileApi 接口,和前面的示例相比没有任何变化,只是为了方便大家查看,这里重复一下。示例代码如下:
163 |
164 | ```java
165 | /**
166 | * 导出的文件对象的接口
167 | */
168 | public interface ExportFileApi {
169 | /**
170 | * 导出内容成为文件
171 | * @param data 示意:需要保存的数据
172 | * @return 是否导出成功
173 | */
174 | public boolean export(String data);
175 | }
176 | ```
177 |
178 | (2) 同样提供「保存成文本文件」和「保存成数据库备份文件」的实现,和前面的示例相比没有任何变化。示例代码如下:
179 |
180 | ```java
181 | public class ExportTxtFile implements ExportFileApi {
182 | public boolean export(String data) {
183 | // 简单地示意一下,这里需要操作文件
184 | System.out.println("导出数据"+data+"到文本文件");
185 | return true;
186 | }
187 | }
188 |
189 | public class ExportDB implements ExportFileApi {
190 | public boolean export(String data) {
191 | // 简单示意一下,这里需要操作数据库和文件
192 | System.out.println("导出数据"+data+"到数据库备份文件");
193 | return true;
194 | }
195 | }
196 | ```
197 |
198 | (3) 接下来该看看 ExportOperate 类了,这个类的变化大致如下。
199 |
200 | - ExportOperate 类中的创建产品的「工厂方法」,通常需要「提供默认的实现」,「不再抽象了」,也就是变成了「正常方法」。
201 | - ExportOperate 类也「不再定义成抽象类」了,因为「有了默认的实现」,客户端可能需要「直接使用这个对象」。
202 | - 设置一个导出类型的参数,通过 export 方法从客户端传入。
203 |
204 | 评论:只用一个~~子类~~ (现在子类也不用了,所以 class 不设置成 abstract 了) 实现之前两个 operate 的任务。
205 |
206 | 看看代码吧,示例代码如下:
207 |
208 | ```java
209 | /**
210 | * 实现导出数据的业务功能对象
211 | */
212 | public class ExportOperate { // ← 不再是抽象类了
213 | /**
214 | * 导出文件
215 | * @param type 用户选择的导出类型
216 | * @param data 需要保存的数据
217 | * @return 是否成功导出文件
218 | */
219 | public boolean export(int type, String data) { // ← 传入参数
220 | // 使用工厂方法
221 | ExportFileApi api = factoryMethod(type);
222 | return api.export(data);
223 | }
224 |
225 | /**
226 | * 工厂方法,创建导出的文件对象的接口对象
227 | * @param type 用户选择的导出类型
228 | * @return 导出的文件对象的接口对象
229 | */
230 | protected ExportFileApi factoryMethod(int type) {
231 | // ↑ 不再抽象了,要提供默认的实现,根据传入的导出类型来选择已有的实现
232 |
233 | ExportFileApi api = null;
234 | // 根据类型来选择究竟要创建哪一种导出文件对象
235 | if (type == 1) {
236 | api = new ExportTxtFile();
237 | } else if (type == 2) {
238 | api = new ExportDB();
239 | }
240 |
241 | return api;
242 | }
243 | }
244 | ```
245 |
246 | (4) 此时的客户端非常简单,直接使用 ExportOperate 类。示例代码如下:
247 |
248 | ```java
249 | public class Client {
250 | public static void main(String[] args) {
251 | // 创建需要使用的 Creator 对象
252 | ExportOperate operate = new ExportOperate();
253 | // 调用输出数据的功能方法,传入选择导出的参数
254 | operate.export(1, "测试数据");
255 | }
256 | }
257 | ```
258 |
259 | 测试看看,然后修改一下客户端的参数,体会一下通过参数来选择具体的导出实现的过程。
260 |
261 | 提示:这是一种「很常见的参数化工厂方法」的实现方式,但是「也还是有把参数化工厂方法实现为抽象的」,这点要注意,「并不是说参数化工厂方法就不能实现为抽象类了」。只是一般情况下,参数化工厂,「在父类就会会提供默认的实现」。
262 |
263 | (5) 扩展新的实现。
264 |
265 | 使用参数化工厂方法,「扩展起来会非常容易」,已有的代码都不会改变,只需要「新加入一个子类来提供新的工厂方法实现」,然后在客户端使用这个新的子类即可。
266 |
267 | 这种实现方式还有一个很有意思的功能,就是「子类可以选择覆盖」,不想覆盖的功能还可以「返回去让父类来实现」,很有意思。
268 |
269 | 扩展一个导出成 xml 文件的示例代码如下:
270 |
271 | ```java
272 | /**
273 | * 导出成 xml 文件的对象
274 | */
275 | public class ExportXml implements ExportFileApi {
276 | public boolean export(String data) {
277 | // 简单示意一下
278 | System.ot.println("导出数据"+data+"到 XML 文件");
279 | return true;
280 | }
281 | }
282 | ```
283 |
284 | 然后扩展 ExportOperate 类,来加入新的实现。示例代码如下:
285 |
286 | ```java
287 | /**
288 | * 扩展 ExportOperate 对象,加入可以导出的 XML 文件
289 | */
290 | public class ExportOperate2 extends ExportOperate {
291 | /**
292 | * 覆盖父类的工厂方法,创建导出的文件对象的接口对象
293 | * @param type 用户选择的导出类型
294 | * @return 导出的文件对象的接口对象
295 | */
296 | protected ExportFileApi factoryMethod(int type) {
297 | ExportFileApi api = null;
298 | // 可以全部覆盖,也可以选择自己感兴趣的覆盖
299 | // 这里只想添加自己新的实现,其他的不管
300 | if (type == 3) {
301 | api = new ExportXml();
302 | } else {
303 | // 其他的还是让父类来实现
304 | api = super.factoryMethod(type);
305 | }
306 | return api;
307 | }
308 | }
309 | ```
310 |
311 | 看看此时的客户端,也非常简单,只是在变换传入的参数。示例代码如下:
312 |
313 | ```java
314 | public class Client {
315 | public static void main(String[] args) {
316 | // 创建需要使用的 Creator 对象
317 | ExportOperate operate = new ExportOperate2();
318 |
319 | // 下面变换传入的参数来测试参数化工厂方法
320 | operate.export(1, "Test1");
321 | operate.export(2, "Test2");
322 | operate.export(3, "Test3");
323 | }
324 | }
325 | ```
326 |
327 | 对应的测试结果如下:
328 |
329 | ```
330 | 导出数据Test1到文本文件
331 | 导出数据Test2到数据库备份文件
332 | 导出数据Test3到XML文件
333 | ```
334 |
335 | 通过上面的示例,好好体会一下参数化工厂方法的实现和带来的好处。
336 |
337 | ### 工厂方法模式的优缺点
338 |
339 | #### 工厂方法模式的优点
340 |
341 | 工厂方法模式的优点
342 |
343 | - 可以在「不知道具体实现的情况下」编程
344 |
345 | 工厂方法模式可以让你实现功能的时候,如果「需要某个产品对象」,只需要「使用产品的接口」即可,而「无需关心具体的实现」。选择具体实现的任务「延迟到子类去完成」。(IoC/DI 思想)
346 |
347 | - 更容易「扩展对象的新版本」
348 |
349 | 工厂方法给子类提供了一个「挂钩」,使得「扩展新的对象变得非常容易」。比如上面示例的参数化工厂方法的实现种,扩展一个新的导出 XML 文件格式的实现,「已有的代码都不会改变」,只要「新加入一个子类」提供新工厂方法实现,然后在客户端使用这个新的子类即可。
350 |
351 | 提示:另外这里提到的挂钩,就是我们经常说的「钩子方法 (Hook)」,这个会在后面讲「模板方法模式」的时候详细点说明。
352 |
353 | - 连接「平行的类层次」
354 |
355 | 工厂方法除了创造产品对象外,在「连接平行的类层次」上也大显身手。这个在前面已经详细讲述了。
356 |
357 | #### 工厂方法模式的缺点
358 |
359 | - 「具体产品对象」和「工厂方法」的耦合性
360 |
361 | 在工厂方法模式中,工厂方法是「需要创建产品对象的」,也就是需要「选择具体的产品对象」,并「创建它们的示例」,因此「具体产品对象和工厂方法是耦合的」。
362 |
363 | ### 思考工厂方法模式
364 |
365 | #### 1. 工厂方法模式的本质
366 |
367 | > 工厂方法模式的本质:延迟到子类来选择实现。
368 |
369 | 仔细体会前面的示例,你会发现,工厂方法模式中的「工厂方法」,在具体的实现的时候,一般是先「选择具体使用哪一个具体的产品实现对象」,然后「创建这个具体产品对象的实例」,最后就可以返回出去了。也就是说,工厂方法「本身并不会去实现产品接口」,具体的产品实现是「已经写好」的,工厂方法「只要去选择实现就好了」。
370 |
371 | 有些朋友可能会说,这个不是跟简单工厂一样吗?
372 |
373 | 从本质上讲,它们确实是「非常类似的」,在具体实现上都是「选择实现」。但是也「存在不同点」,简单工厂是「直接在工厂类里面进行 “选择实现”」;而工厂方法会把这个工作「延迟到子类来实现」,工厂类里面「使用工厂方法的地方」是「依赖于抽象而不是具体的实现」,从而使得系统更加灵活,「具有更好的可维护和可扩展性」。
374 |
375 | 其实如果把工厂模式的 Creator 退化一下,只提供工厂方法,而且这些方法还都提供默认的实现,那不就变成简单工厂了吗?比如把刚才示范参数化工厂方法的例子代码拿过来再简化一下,你就能看出来,写得跟简单工厂是差不多的。实例代码如下:
376 |
377 | ```java
378 | /**
379 | * 实现导出数据的业务功能对象
380 | */
381 | public class ExportOperate {
382 |
383 | // ↓↓↓ 简化这个 Creator,把这些都注释 ↓↓↓
384 | // /**
385 | // * 导出文件
386 | // * @param type 用户选择的导出类型
387 | // * @param data 需要保存的数据
388 | // * @return 是否成功导出文件
389 | // */
390 | // public boolean export(int type, String data) {
391 | // // 使用工厂方法
392 | // ExportFileApi api = factoryMethod(type);
393 | // return api.export(data);
394 | // }
395 |
396 | /**
397 | * 工厂方法,创建导出的文件对象的接口对象
398 | * @param type 用户选择的导出类型
399 | * @return 导出的文件对象的接口对象
400 | */
401 |
402 | // ↓ 留下的这个方法,如果把它修改成 public static 的,
403 | // ↓ 是不是就和简单工厂写得一样了?
404 | protected ExportFileApi factoryMethod(int type) {
405 | ExportFileApi api = null;
406 | // 根据类型来选择究竟要创建哪一种导出文件对象
407 | if (type == 1) {
408 | api = new ExportTxtFile();
409 | } else if (type == 2) {
410 | api = new ExportDB();
411 | }
412 |
413 | return api;
414 | }
415 | }
416 | ```
417 |
418 | 看完上述代码,会体会到简单工厂和工厂方法模式是「有很大的相似」的了吧。从某个角度来讲,可以认为简单工厂就是工厂方法模式的「一种特例」,因此它们的本质是类似的,也就不足为奇了。
419 |
420 | #### 2. 对设计原则的实现
421 |
422 | 工厂方法模式很好地体现了「依赖倒置原则」。
423 |
424 | 依赖倒置原则告诉我们「要依赖抽象,不要依赖于具体类」,简单点说就是:「不能让高层组件依赖于低层组件」,而且不管高层组件还是底层组件,都应该「依赖于抽象」。
425 |
426 | 比如前面的示例,实现客户端请求操作的 ExportOperate 就是「高层组件」;而「具体实现数据导出的对象」就是底层组件,比如 ExportTxtFile、ExportDB;而 ExportFileApi 接口就相当于是那个抽象。
427 |
428 | 对于 ExportOperate 来说,它「不关心具体的实现方式」,它只是「面向接口编程」;对于具体的实现来说,它只关心「如何实现接口所要求的功能」。
429 |
430 | 那么「倒置的是什么呢」?倒置的是这个接口的「所有权」。事实上,ExportFileApi 接口中定义的功能,都是由高层组件 ExportOperate 来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件「只是提出要求」,并不关系如何实现,而底层组件,就是来真正实现高层组件所要求的接口功能的。因此看来,底层实现的接口的所有权并不在底层组件手中,而是「倒置到高层组件去了」。(高层组件牵制着底层组件)
431 |
432 | #### 3. 何时选用工厂方法模式
433 |
434 | 建议在以下情况中选择工厂方法模式。
435 |
436 | - 如果一个类「需要创建某个接口的对象」,但是又「不知道具体的实现」,这种情况可以选用工厂方法模式,把创建对象的工作「延迟到子类中」去实现。
437 | - 如果一个类「本身就希望由它的子类来创建」所需的对象的时候,应该使用工厂方法模式。
438 |
439 | ### 相关模式
440 |
441 | - 工厂方法模式和抽象工厂模式
442 |
443 | 这两个模式可以「组合使用」,具体的放到抽象工厂模式中去讲。
444 |
445 | - 工厂方法模式和模板方法模式
446 |
447 | 这两个模式外观类似,都有一个「抽象类」,然后由「子类」来提供一些实现,但是工厂方法模式的子类「专注的是创建产品对象」,而模板方法模式的子类「专注的是为了固定的算法骨架提供某些步骤的实现」。
448 |
449 | 这两个模式可以「组合使用」,通常在「模板方法模式里面」,使用工厂方法来「创建模板方法需要的对象」。
450 |
--------------------------------------------------------------------------------
/6_抽象工厂模式_2.md:
--------------------------------------------------------------------------------
1 | ## 模式讲解
2 |
3 | ### 认识抽象工厂模式
4 |
5 | #### 1. 抽象工厂模式的功能
6 |
7 | 抽象工厂的功能是「为一系列相关对象」或「相互依赖的对象」创建一个接口。一定要注意,这个接口内的方法「不是任意堆砌的」,而是一系列「相关或相互依赖的方法」,比如上面例子中的 CPU 和主板,都是为了「组装一台电脑」的相关对象。
8 |
9 | 从某种意义上看,抽象工厂其实是「一个产品系列」,或者是「产品簇」。上面例子中的「抽象工厂」就可以看成是「电脑簇」,「每个不同的装机方案,代表一种具体的电脑系列」。
10 |
11 | #### 2. 实现成接口
12 |
13 | 「标星」AbstractFactory 在 Java 中「通常实现为接口」,大家不要被名称误导了,以为是实现为「抽象类」。当然,如果「需要为这个产品簇提供公共的功能」,也不是不可以把 AbstractFactory 实现成为「抽象类」,但「一般不这么做」。
14 |
15 | #### 3. 使用工厂方法
16 |
17 | AbstractFactory 定义「创建产品所需要的接口」,具体的实现是「在实现类里面」,通常在「实现类」里面就需要「选择多种更具体的实现」。所以 AbstractFactory 定义的「创建产品」的方法可以看成是「工厂方法」,而这种工厂方法的「具体实现」就「延迟到了具体的工厂里面」。也就是说,「使用工厂方法来实现抽象工厂」。
18 |
19 | #### 4. 切换产品簇
20 |
21 | 由于抽象工厂定义的一系列对象通常是「相关或者相互依赖的」,这些产品对象就构成了一个「产品簇」,也就是「抽象工厂定义了一个产品簇」。
22 |
23 | 这就带来了非常大的「灵活性」,切换一个产品簇的时候,只要「提供不同的抽象工厂实现 (就是前面的 Schema 装机方案)」就可以了,也就是说现在以产品簇作为一个整体被切换。
24 |
25 | #### 5. 抽象工厂模式的调用顺序示意图
26 |
27 | 抽象工厂模式的调用顺序如图所示:
28 |
29 | 【待补充】[P141]
30 |
31 | ### 定义可扩展的工厂
32 |
33 | 在前面的示例中,抽象工厂为每一种「它能创建的对象」定义了相应的方法,比如「创建 CPU 的方法」和「创建主板的方法」等。
34 |
35 | 这种实现由一个麻烦,就是「如果在产品簇种要增加一种产品」,比如现在要求抽象工厂能够除了创建 CPU 和主板外,还要「能够创建内存对象」,那么就需要「在抽象工厂里面添加创建内存的一个方法」。当抽象工厂「**一发生改变,所有的具体工厂实现都要发生变化**」,如下、如此就非常的「不灵活」。
36 |
37 | 现在又一种相对灵活,但「不太安全」的改进方式可以解决这个问题,思路如下:抽象工厂里面不需要定义那么多方法,「定义一个方法就可以了」,给这个方法「设置一个参数,通过这个参数来判断具体创建什么产品对象」;由于只有一个方法,在返回类型上就不能是具体的某个产品类型了,只能是所有产品对象都继承或者实现的那么一个类型,比如让所有产品都实现某个接口,或者「干脆直接使用 Object 类型」、
38 |
39 | 还是通过代码来体会一下,把前面那个示例改造成能够扩展的工厂实现。
40 |
41 | (1) 先来改造抽象工厂。示例代码如下:
42 |
43 | ```java
44 | /**
45 | * 可扩展的抽象工厂接口
46 | */
47 | public interface AbstractFactory {
48 | /**
49 | * 一个通用的创建产品对象的方法,为了简单,直接返回 Object
50 | * 也可以为所有被创建的产品定义一个公共的接口
51 | * @param type 具体创建产品类型标识
52 | * @return 创建出的产品对象
53 | */
54 | public Object createProduct(int type);
55 | }
56 | ```
57 |
58 | (2) CPU 的接口和实现。主板的接口和实现和前面的示例一样,这里就不再示范了。CPU 分为 Intel 的 CPU 和 AMD 的 CPU,主板分为华硕的主板和微星的主板。
59 |
60 | 注意:这里要特别注意传入 createProduct 的参数所代表的含义,这个参数只是用来标识现在创建什么类型的产品,比如标识现在是创建 CPU 还是创建主板。一般这个 type 的含义到此就结束了,不再进一步标识具体是什么样的 CPU 或具体是什么样的主板。也就是说「type 不再标识具体是创建 Intel 的 CPU 还是创建 AMD 的 CPU」,这就是一个参数所代表的「含义的深度问题」。要注意,虽然也可以延伸参数的含义到具体的实现上,但这不是可扩展工厂这种设计方式的本意,「一般也不这么去做」。
61 |
62 | (3) 下面来提供具体的工厂实现,也就是相当于以前的装机方案。
63 |
64 | 先改造原来的方案一把,现在的实现会有较大的变化。示例代码如下:
65 |
66 | ```java
67 | /**
68 | * 装机方案一:Intel 的 CPU + 华硕的主板
69 | * 这里创建 CPU 和主板对象的时候,是对应的,能匹配上的
70 | */
71 | public class Schema1 implements AbstractFactory {
72 | public Object createProduct(int type) {
73 | Object retObj = null;
74 |
75 | // type 为 1 标识创建 CPU,type 为 2 表示创建主板
76 | if (type == 1) {
77 | retObj = new IntelCPU(1156);
78 | } else if (type == 2) {
79 | retObj = new AsusMotherboard(1156);
80 | }
81 |
82 | return retObj;
83 | }
84 | }
85 | ```
86 |
87 | 用同样的方式来改造原来的方案二。示例代码如下:
88 |
89 | ```java
90 | /**
91 | * 装机方案二:AMD 的 CPU + 微星的主板
92 | * 这里创建 CPU 和主板对象的时候,是对应的,能匹配上的
93 | */
94 | public class Schema2 implements AbstractFactory {
95 | public Object createProduct(int type) {
96 | Object retObj = null;
97 |
98 | // type 为 1 标识创建 CPU,type 为 2 表示创建主板
99 | if (type == 1) {
100 | retObj = new AMDCPU(939);
101 | } else if (type == 2) {
102 | retObj = new MSIMotherboard(939);
103 | }
104 |
105 | return retObj;
106 | }
107 | }
108 | ```
109 |
110 | (4) 在这个时候使用抽象方法的客户端实现,也就是在装机工程师类里面,通过抽象工厂来获取相应的配件产品对象。示例代码如下:
111 |
112 | ```java
113 | public class ComputerEngineer {
114 | private CPUApi cpu = null;
115 | private MotherboardApi motherboard = null;
116 |
117 | public void makeComputer(AbstractFactory schema) {
118 | prepareHardwares(schema);
119 | }
120 |
121 | private void prepareHardwares(AbstractFactory schema) {
122 | // 这里要去准备 CPU 和主板的具体实现,为了示例简单,这里只准备这两个
123 | // 可是,装机工程师并不知道如何去创建,怎么办?
124 |
125 | // 使用抽象工厂来获取相应的接口对象
126 | this.cpu = (CPUApi)schema.createProduct(1); // ← 蓝色代码
127 | this.motherboard = (MotherboardApi)schema.createProduct(2); // ← 蓝色代码
128 |
129 | // 测试一下配件是否好用
130 | this.cpu.calculate();
131 | this.motherboard.installCPU();
132 | }
133 | }
134 | ```
135 |
136 | 通过上面的示例,能看到可扩展工厂的基本实现。从客户端的代码会发现,为什么说这种方式是不太安全的?
137 |
138 | 评论:Java 中所有类都默认继承了 Object 这个类,使用 Object 类型涉及类型的转换,类型属于是 any,潜在未知的问题。
139 |
140 | 仔细查看上面的「蓝色代码」,会发现什么?
141 |
142 | 你会发现创建产品对象返回后,需要类型为具体的对象,因为返回的是 Object,如果这个时候没有匹配上,比如返回的不是 CPU 对象,但是要强制类型成为 CPU,那么就会发生错误,因此这种实现方式的一个潜在风险就是不太安全。
143 |
144 | (5) 下面来体会一下这种方式的灵活性
145 |
146 | 假如现在要加入一个新产品 —— 内存,当然也可以提供一个新的装机方案来使用它,这里已有的代码就不需要变化了。
147 |
148 | 内存接口的示例代码如下:
149 |
150 | ```java
151 | /**
152 | * 内存的接口
153 | */
154 | public interface MemoryApi {
155 | /**
156 | * 示意方法,内存具有缓存数据的能力
157 | */
158 | public void cacheData();
159 | }
160 | ```
161 |
162 | 提供一个镁光内存的基本实现。示例代码如下:
163 |
164 | ```java
165 | /**
166 | * 镁光内存的类
167 | */
168 | public class MicronMemory implements MemoryApi {
169 | public void cacheData() {
170 | System.out.prinln("现在正在使用镁光内存");
171 | }
172 | }
173 | ```
174 |
175 | 现在若要使用这个新加入的产品,以前实现的代码就不用变化,只需要添加一个方案,在这个方案里面使用新的产品,然后客户端使用这个新的方案即可,示例代码如下:
176 |
177 | ```java
178 | /**
179 | * 装机方案三:Intel 的 CPU + 华硕的主板 + 镁光的内存
180 | */
181 | public class Schema3 implements AbstractFactory {
182 | public Object createProduct(int type) {
183 | Object retObj = null;
184 | // type 为 1 表示创建 CPU,2 表示创建主板,3 表示创建内存
185 | if (type == 1) {
186 | retObj = new IntelCpu(1156);
187 | } else if (type == 2) {
188 | retObj = new AsusMotherboard(1156);
189 | }
190 | // 创建新添加的产品
191 | else if (type == 3) {
192 | retObj = new MicronMemory();
193 | }
194 |
195 | return retObj;
196 | }
197 | }
198 | ```
199 |
200 | 这个时候的装机工程师类,如果要「创建带内存的电脑」,需要在装机工程师类里面「添加对内存的使用」。示例代码如下:
201 |
202 | ```java
203 | public class ComputerEngineer {
204 | private CPUApi cpu = null;
205 | private MotherboardApi = motherboard =null;
206 |
207 | /**
208 | * 定义组装电脑需要的内存
209 | */
210 | private MemoryApi memory = null;
211 |
212 | public void makeComputer(AbstractFactory schema) {
213 | prepareHardwares(schema);
214 | }
215 |
216 | private void prepareHardwares(AbstractFactory schema) {
217 | // 使用抽象工厂来获取的接口对象
218 | this.cpu = (CPUApi)schema.createProduct(1);
219 | this.motherboard = (MotherboardApi)schema.createProduct(2);
220 | this.memory = (MemoryApi)schema.createProduct(3);
221 |
222 | // 测试一下配件是否好用
223 | this.cpu.calculate();
224 | this.motherboard.installCPU();
225 |
226 | if (memory != null) { // ←↓ 蓝色代码
227 | this.memory.cacheData();
228 | }
229 | }
230 | }
231 | ```
232 |
233 | 可能有朋友会发现上面「蓝色代码」中内存操作的地方,跟前面 CPU 和主板的操作方式不一样,「多了一个 if 判断」。原因是为了要「同时满足以前和现在的要求」,如果是以前的客户端,它调用的时候「就没有内存」,这个时候操作内存「就会出错」,因此「添加一个判断」,有内存的时候才操作内存,就不会出错了。
234 |
235 | 此时客户端,只要选择使用方案三就可以了。示例代码如下:
236 |
237 | ```java
238 | public class Client {
239 | public static void main(String[] args) {
240 | ComputerEngineer engineer = new ComputerEngineer();
241 | AbstractFactory schema = new Schema3(); // ←
242 | engineer.makeComputer(schema);
243 | }
244 | }
245 | ```
246 |
247 | 运行结果如下:
248 |
249 | ```
250 | now in Intel CPU, pins=1156
251 | now in AsusMotherboard, cpuHoles=1156
252 | 现在正在使用镁光内存
253 | ```
254 |
255 | 测试一下看看,体会一下这种设计方式的「灵活性」。当然前面也讲到了,这种方式「可能会不太安全」,至于是否使用,就看「具体应用设计上的权衡了」。
256 |
257 | 评论:
258 |
259 | 抽象工厂模式把很多零散的产品变成一个「产品族 (也就是上面的 Schema)」,在整个「产品族」层次间能灵活地快速切换;但是由于 interface 要求实现其中定义的所有方法,如果当产品族「里面」加入新的产品 (也就是上面的内存),这时所有 interface 的实现都需要改变,这样就不灵活了,只有通过上面的骚操作,但存在类型转换的安全性问题,设计使用的时候需要权衡考量。
260 |
261 | ### 抽象工厂模式和 DAO
262 |
263 | #### 1. 什么是 DAO
264 |
265 | DAO:数据访问对象,是 Data Access Object 首字母的简写。
266 |
267 | DAO 是 JEE (也称 JavaEE,原 J2EE) 中的一个「标准模式」,通过它来「解决访问数据对象所面临的一系列问题」,比如,数据源不同、存储类型不同、访问方式不同、供应商不同、版本不同等,这些不同会「造成访问数据的实现上差别很大」。
268 |
269 | - 数据源的不同,比如存放于「数据库」的数据源,存放于「LDAP (轻型访问协议)」的数据源;又比如存放于「本地数据源」和「远程服务器」上的数据源等。
270 | - 存储类型的不同,比如关系型数据库 (RDBMS)、面向对象数据库 (ODBMS)、纯文本、XML 等。
271 | - 访问方式的不同,比如访问关系型数据库,可以用 JDBC、EntityBean、JPA 等来实现,当然也可以采用一些流行的框架,如 Hibernate、IBatis 等。
272 | - 供应商的不同,比如关系型数据库,流行的如 Oracle、DB2、SQLServer、MySQL 等等,它们的供应商是不同的。
273 | - 版本不同,比如关系型数据库,不同的版本,实现的功能是有差异的,就算是对标准的 SQL 的支持,也是有差异的。
274 |
275 | 但是对于「需要进行数据访问」的「逻辑层」而言,它可不想面对这么多不同,也不想处理这么多差异,它希望能以一个「统一的方式」来访问数据库。
276 |
277 | 此时结构如图所示:
278 |
279 | 【待补充】[P146]
280 |
281 | 也就是说,DAO 需要「抽象和封装所有对数据的访问」,DAO 承担「和数据仓库交互的职责」,这也意味着,访问数据所面临的「所有问题,都需要 DAO 在内部来自行解决」。
282 |
283 | 评论:是不是有点像 Facade 模式。Facade 屏蔽了外部客户端和系统内部模块的交互,简化了客户端的调用。
284 |
285 | #### 2. DAO 和抽象工厂的关系
286 |
287 | 「标星」了解了什么是 DAO 后,可能有些朋友会像,DAO 同抽象工厂模式有什么关系呢?看起来好像完全不靠边啊。
288 |
289 | 事实上,在实现 DAO 模式的时候,「最常见的实现策略」就是「使用工厂的策略」,而且「多是通过抽象工厂模式来实现」,当然在使用抽象工厂模式来实现的时候,可以结合工厂方法模式。因此「DAO 模式和抽象工厂模式有很大的联系」。
290 |
291 | #### 3. DAO 模式的工厂实现策略
292 |
293 | 下面就来看看 DAO 模式实现的时候是「如何采用工厂方法和抽象工厂」的。
294 |
295 | **1) 采用工厂方法模式**
296 |
297 | 假如现在在一个订单处理模块里面。大家都知道,订单通常分为两个部分,一部分是「订单主记录」或者是「订单主表」,另一部分是「订单明细记录」或者是「订单子表」,那么现在业务对象需要「操作订单的主记录」,也需要「操作订单的子记录」。
298 |
299 | 如果这个时候的「业务比较简单」,而且对数据的「操作是固定的」,比如就是操作数据库,不管订单的业务如何变化,「底层数据存储都是一样的」,那么这种情况下,可以采用「工厂方法」模式,此时的系统结构如图:
300 |
301 | 【待补充】[P147]
302 |
303 | 从上面的结构示意图可以看出,如果「底层存储固定」的时候,DAOFactory 就相当于「工厂方法模式」中的 Creator,在里面定义两个工厂方法,分别「创建订单主记录」的 DAO 对象和「创建订单子记录」的 DAO 对象,因为「固定的是数据库实现」,因此提供一个「具体的工厂」RdbDAOFactory (Rdb,关系型数据库) 来「实现对象的创建」,也就是说 DAO 可以「采用工厂方法模式来实现」。
304 |
305 | 采用工厂方法模式的情况,要求 DAO 底层「存储实现方式是固定的」,这种模式多用在一些「简单的小项目的开发上」。
306 |
307 | **2) 采用抽象工厂模式**
308 |
309 | 实际上更多的时候 DAO 底层存储方式是「不固定的」,DAO 通常会「支持多种存储」的实现方式,具体使用哪一种存储方式可能是由应用「动态决定」的,或者是「通过配置来指定」。这种情况多见于产品开发,或者是「稍复杂的应用、亦或较大的项目中」。
310 |
311 | 对于底层存储方式不固定的时候,一般采用「抽象工厂」模式来实现 DAO。比如现在实现「除了 RDB 的实现」,还会由 XML 的实现,它们「会被应用动态的选择」,此时系统结构如图:
312 |
313 | 【待补充】[P148]
314 |
315 | 从上面的结构示意图可以看出,采用「抽象工厂」模式来「实现 DAO 的时候」,DAOFactory 就相当于抽象工厂,里面定义一系列创建相关对象的方法,分别是「创建订单主记录 DAO 对象」和「创建订单子记录的 DAO 对象」,此时 OrderMainDAO 和 OrderDetailDAO 就相当于「被创建的产品」,RdbDAOFactory 和 XmlDAOFactory 就相当于「抽象工厂的具体实现」,在它们里面会「选择相应的具体的产品实现」来创建对象。
316 |
317 | #### 4. 代码示例使用抽象工厂实现 DAO 模式
318 |
319 | (1) 先看看抽象工厂的代码实现。示例代码如下:
320 |
321 | ```java
322 | /**
323 | * 抽象工厂,创建订单主、子记录对应的 DAO 对象
324 | */
325 | public abstract class DAOFactory {
326 | /**
327 | * 创建订单主记录对应的 DAO 对象
328 | * @return 订单主记录对应的 DAO 对象
329 | */
330 | public abstract OrderMainDAO createOrderMainDAO();
331 |
332 | /**
333 | * 创建订单子记录对应的 DAO 对象
334 | * @return 订单子记录对应的 DAO 对象
335 | */
336 | public abstract OrderDetailDAO createOrderDetailDAO();
337 | }
338 | ```
339 |
340 | (2) 看看产品对象的接口,就是订单主、子记录的 DAO 定义。
341 |
342 | 先来看看订单主记录的 DAO 定义。示例代码如下:
343 |
344 | ```java
345 | /**
346 | * 订单主记录对应的 DAO 操作接口
347 | */
348 | public interface OrderMainDAO {
349 | /**
350 | * 示意方法,保存订单主记录
351 | */
352 | public void saveOrderMain();
353 | }
354 | ```
355 |
356 | ```java
357 | /**
358 | * 订单子记录对应的 DAO 操作接口
359 | */
360 | public interface OrderDetailDAO {
361 | /**
362 | * 示例方法,保存订单子记录
363 | */
364 | public void saveOrderDetail();
365 | }
366 | ```
367 |
368 | (3) 接下来实现订单主、子记录的 DAO。
369 |
370 | 先来看看「关系型数据库」的实现方式,示例代码如下:
371 |
372 | ```java
373 | public class RdbMainDAOImpl implements OrderMainDAO {
374 | public void saveOrderMain() {
375 | System.out.println("now in RdbMainDAOImpl saveOrderMain");
376 | }
377 | }
378 |
379 | public class RdbDetailDAOImpl implements OrderDetailDAO {
380 | public void saveOrderDetail() {
381 | System.out.println("now in RdbDetailDAOImpl saveOrderMain");
382 | }
383 | }
384 | ```
385 |
386 | XML 代码的实现方式一样。为了演示简单,就是输出了一句话。示例代码如下:
387 |
388 | ```java
389 | public class XmlMainDAOImpl implements OrderMainDAO {
390 | public void saveOrderMain() {
391 | System.out.println("now in XmlMainDAOImpl saveOrderMain");
392 | }
393 | }
394 |
395 | public class XmlDetailDAOImpl implements OrderDetailDAO {
396 | public void saveOrderDetail() {
397 | System.out.println("now in XmlDAOImpl2 saveOrderDetail");
398 | }
399 | }
400 | ```
401 |
402 | (4) 再看看具体的工厂实现。
403 |
404 | 先来看看关系型数据库实现方式的工厂。示例代码如下:
405 |
406 | ```java
407 | public class RdbDAOFactory extends DAOFactory {
408 | public OrderDetailDAO createOrderDetailDAO() {
409 | return new RdbDetailDAOImpl();
410 | }
411 |
412 | public OrderMainDAO createOrderMainDAO() {
413 | return new RdbMainDAOImpl();
414 | }
415 | }
416 | ```
417 |
418 | XML 实现方式的工厂的示例代码如下:
419 |
420 | ```java
421 | public class XmlDAOFactory extends DAOFactory {
422 | public OrderDetailDAO createOrderDetailDAO() {
423 | return new XmlDetailDAOImpl();
424 | }
425 |
426 | public OrderMainDAO createOrderMainDAO() {
427 | return new XmlMainDAOImpl();
428 | }
429 | }
430 | ```
431 |
432 | (5) 好了,使用抽象工厂简单地实现了 DAO 模式。在客户端通常是由业务对象来调用 DAO,那么该怎么使用这个 DAO 呢?示例代码如下:
433 |
434 | ```java
435 | public class BusinessObject {
436 | public static void main(String[] args) {
437 | // 创建 DAO 的抽象工厂
438 | DAOFactory df = new RdbDAOFactory();
439 |
440 | // 通过抽象工厂来获取需要的 DAO 接口
441 | OrderMainDAO mainDAO = df.createOrderMainDAO();
442 | OrderDetailDAO detailDAO = df.createOrderMainDAO();
443 |
444 | // 调用 DAO 来完成数据存储的功能
445 | mainDAO.saveOrderMain();
446 | detailDAO.saveOrderDetail();
447 | }
448 | }
449 | ```
450 |
451 | 通过上面的示例,可以看出 DAO 可以「采用抽象工厂模式」来实现,这也是「大部分 DAO 实现所采用的方式」。
452 |
453 | ### 抽象工厂模式的优缺点
454 |
455 | **抽象工厂模式的优点**
456 |
457 | - 分离接口和实现
458 |
459 | 客户端使用抽象工厂来「创建需要的对象」,而客户端「根本就不知道具体的实现是谁」,客户端只是「面向产品的接口编程而已」。也就是说,客户端「从具体的产品实现中解耦」。
460 |
461 | - 使得「切换产品簇」变得容易
462 |
463 | 因为一个「具体的工厂实现」代表的是一个「产品簇」,比如上面例子的 Scheme1 代表装机方案一:Intel 的 CPU + 华硕的主板,如果要切换成 Scheme2,那就变成了装机方案二:AMD 的 CPU + 微星主板。
464 |
465 | 客户端「选用不同的工厂实现」,就相当于是在「切换不同的产品簇」。
466 |
467 | **抽象工厂模式的缺点**
468 |
469 | - 不太容易扩展新产品
470 |
471 | 前面也提到了这个问题,如果需要给「整个产品簇」添加一个新的产品,那么就需要「修改抽象工厂」,这样就会导致「修改所有的工厂实现类」。在前面提供了一个可以扩展工厂的方式来解决这个问题,但是又「不够安全」。如何选择,则要「根据实际应用来权衡」。
472 |
473 | - 容易造成类层次复杂
474 |
475 | 在使用抽象工厂模式的时候,如果需要选择的层次过多,那么会造成「整个类层次变得复杂」。
476 |
477 | 举个例子来说,就比如前面讲到的 DAO 的示例,现在这个 DAO 只有一个选择的层次,也就是选择是使用「关系型数据库」来实现,还是用「XML」来实现。现在考虑这样一个问题,如果关系型数据库实现里面「又分成几种」,比如,基于 Oracle 的实现、基于 SqlServer 的实现、基于 MySQL 的实现等。
478 |
479 | 那么客户端怎么选择呢?不会把所有的可能的实例情况全部都做到一个层次上把,这个时候客户端就需要「一层一层地选择」,也就是「整个抽象工厂的实现也需要分出层次来,每一层负责一种选择,也就是一层屏蔽一种变化」,这样很容易造成复杂的类层次结构。
480 |
481 | ### 思考抽象工厂模式
482 |
483 | > 抽象工厂模式的本质:选择产品簇的实现。
484 |
485 | #### 1. 抽象工厂模式的本质
486 |
487 | 「工厂方法」是选择「单个产品」的实现,虽然一个类里面可以有「多个工厂方法」,但是这些方法之间「一般是没有联系的」,即使看起来像有联系。
488 |
489 | 但是「抽象工厂」着重的就是为一个「产品簇」选择实现,定义在抽象工厂里面的方法「通常是有联系的」,它们都是「产品的某一部分」或者是「相互依赖的」。如果「抽象工厂」里面定义一个方法,「直接创建产品」,那么就「退化成为工厂方法」了。
490 |
491 | #### 2. 何时选用抽象工厂模式
492 |
493 | 建议在以下情况中选用抽象工厂模式。
494 |
495 | - 如果希望「系统独立于它的产品的创建、组合和表示的时候」。换句话说,希望一个系统「只是知道产品的接口,而不关心实现的时候」。
496 | - 如果一个系统要「由多个产品系列中的一个来配置的时候」。换句话说,就是「可以动态地切换产品簇的时候」。
497 | - 如果要强调一系列相关产品的接口,以便「联合使用它们」的时候。
498 |
499 | ### 相关模式
500 |
501 | - 抽象工厂和工厂方法模式
502 |
503 | 这两个模式「既有区别,又有联系」,可以组合使用。
504 |
505 | 工厂方法模式一般「针对单独的产品对象的创建」,而抽象工厂模式「注重产品簇对象的创建」,这是它们的区别。
506 |
507 | 如果把抽象工厂创建的产品簇「简化」,这个产品簇就「只有一个产品」,那么这个时候的抽象工厂跟工厂方法是差不多的,也就是抽象工厂「可以退化成工厂方法」,而工厂方法又「可以退化成简单工厂」,这也是它们的联系。
508 |
509 | 在抽象工厂的实现中,还可以使用工厂方法来提供抽象工厂的具体实现,也就是说它们可以「组合使用」。
510 |
511 | - 抽象工厂模式和单例模式
512 |
513 | 这两个模式可以组合使用。
514 |
515 | 在抽象工厂模式中,具体的工厂实现,在整个应用中,通常「一个产品系列只需要一个实例就可以了,因此可以把具体的工厂实例实现成为单例」。
516 |
--------------------------------------------------------------------------------
/6_抽象工厂模式.md:
--------------------------------------------------------------------------------
1 | ## 抽象工厂模式 (Abstract Factory)
2 |
3 | ## 场景问题
4 |
5 | ### 选择组装电脑的配件
6 |
7 | 举个生活中常见的例子 —— 组装电脑,我们在组装电脑的时候,通常需要选择一系列的配件,比如 CPU、硬盘、内存、主板、电源、机箱等。为了使讨论简单点,只考虑选择 CPU 和主板的问题。
8 |
9 | 事实上,在选择 CPU 的时候,面临一系列的问题,比如「品牌、型号、针脚数目、主频等问题」,只有把这些「都确定下来」,才能确定「具体的 CPU」。
10 |
11 | 同样,在选择主板的时候,也有一系列问题,比如「品牌、芯片组、集成芯片、总线频率等问题」,只有这些「都确定下来了」,才能确定「具体的主板」。
12 |
13 | 选择不同的 CPU 和主板,是每个客户在组装电脑的时候,「向装机公司提出的要求」,也就是我们每个人「自己拟定的装机方案」。
14 |
15 | 在「最终确定这个装机方案」之前,还需要「整体考虑各个配件之间的兼容性」,比如,CPU 和主板,如果 CPU 针脚数和主板提供的 CPU 插口不兼容,是无法组装的。也就是说,装机方案是「有整体性的」,里面选择的「各个配件之间是有关联的」。
16 |
17 | 对于装机工程师而言,他「只知道组装一台电脑,需要相应的配件」,但是「具体使用什么样的配件,还得由客户说了算」。就是说装机工程师「只是负责组装」,而客户「负责选择」装配所需要的配件。因此,让装机工程师为不同的客户组装电脑时,只需要按照客户的装机方案,去获取相应的配件,然后组装即可。
18 |
19 | 现在需要使用程序来把这个装机的过程,尤其是「选择组装电脑配件」的过程实现出来,该如何实现呢?
20 |
21 | ### 不用模式的解决方案
22 |
23 | 考虑客户的功能,需要选择自己需要的 CPU 和主板,然后告诉装机工程师自己的选择,接下来就等着装机工程师组装电脑了。
24 |
25 | 对于装机工程师而言,只是「知道 CPU 和主板的接口」,而「不知道具体的实现」,很明显可以用上「简单工厂」或「工厂方法模式」。为了简单,这里选用「简单工厂」。客户端告诉装机工程师自己的选择,然后装机工程师会「通过相应的工厂」去获取相应的实例对象。
26 |
27 | (1) 下面来看看 CPU 和主板的接口。
28 |
29 | CPU 接口定义的实例代码如下:
30 |
31 | ```java
32 | /**
33 | * CPU 的接口
34 | */
35 | public interface CPUApi {
36 | /**
37 | * 示意方法,CPU 具有运算的能力
38 | */
39 | public void calculate();
40 | }
41 | ```
42 |
43 | 再看看主板的接口定义。示例代码如下:
44 |
45 | ```java
46 | /**
47 | * 主板的接口
48 | */
49 | public interface Motherboard {
50 | /**
51 | * 示意方法,主板都具有安装 CPU 的功能
52 | */
53 | public void installCPU();
54 | }
55 | ```
56 |
57 | (2) 下面来看看具体的 CPU 的实现
58 |
59 | Intel CPU 实现的示例代码如下:
60 |
61 | ```java
62 | /**
63 | * Intel 的 CPU 实现
64 | */
65 | public class IntelCPU implements CPUApi {
66 | /**
67 | * CPU 的针脚数目
68 | */
69 | private int pins = 0;
70 |
71 | /**
72 | * 构造方法,传入 CPU 的针脚数目
73 | * @param pins CPU 的针脚数目
74 | */
75 | public IntelCPU(int pins) {
76 | this.pins = pins;
77 | }
78 |
79 | public void calculate() {
80 | System.out.println("now in Intel CPU, pins="+ pins);
81 | }
82 | }
83 | ```
84 |
85 | 再看看 AMD 的 CPU 实例。示例代码如下:
86 |
87 | ```java
88 | /**
89 | * AMD 的 CPU 实现
90 | */
91 | public class AMDCPU implements CPUApi {
92 | /**
93 | * CPU 的针脚数目
94 | */
95 | private int pins = 0;
96 |
97 | /**
98 | * 构造方法,传入 CPU 的针脚数目
99 | * @param pins CPU 的针脚数目
100 | */
101 | public AMDCPU(int pins) {
102 | this.pins = pins;
103 | }
104 |
105 | public void calculate() {
106 | System.out.println("now in AMD CPU,pins="+pins);
107 | }
108 | }
109 | ```
110 |
111 | (3) 下面来看看具体的主板实现。
112 |
113 | 华硕主板实现的示例代码如下:
114 |
115 | ```java
116 | /**
117 | * 华硕主板
118 | */
119 | public class AsusMotherboard implements MotherboardApi {
120 | /**
121 | * CPU 插槽的孔数
122 | */
123 | private int cpuHoles = 0;
124 |
125 | /**
126 | * 构造方法,传入 CPU 插槽的孔数
127 | * @param cpuHoles CPU 插槽的孔数
128 | */
129 | public AsusMotherboard(int cpuHoles) {
130 | this.cpuHoles = cpuHoles;
131 | }
132 |
133 | public void installCPU() {
134 | System.out.println("now in AsusMotherboard, cpuHoles="+cpuHoles);
135 | }
136 | }
137 | ```
138 |
139 | 微星主板实现的示例代码如下:
140 |
141 | ```java
142 | /**
143 | * 微星主板
144 | */
145 | public class MSIMotherboard implements MotherboardApi {
146 | /**
147 | * CPU 插槽的孔数
148 | */
149 | private int cpuHoles = 0;
150 |
151 | /**
152 | * 构造方法,传入 CPU 插槽的孔数
153 | * @param cpuHoles CPU 插槽的孔数
154 | */
155 | public MSIMotherboard(int cpuHoles) {
156 | this.cpuHoles = cpuHoles;
157 | }
158 |
159 | public void installCPU() {
160 | System.out.println("now in MSIMotherboard, cpuHoles="+cpuHoles);
161 | }
162 | }
163 | ```
164 |
165 | (4) 下面来看看创建 CPU 和主板的工厂。
166 |
167 | 创建 CPU 工厂实现的示例代码如下:
168 |
169 | ```java
170 | /**
171 | * 创建 CPU 的简单工厂
172 | */
173 | public class CPUFactory {
174 | /**
175 | * 创建 CPU 接口对象的方法
176 | * @param type 选择 CPU 类型的参数
177 | * @return CPU 接口对象的方法
178 | */
179 | public static CPUApi createCPUApi(int type) {
180 | CPUApi cpu = null;
181 | // 根据参数来选择并创建相应的 CPU 对象
182 | if (type == 1) {
183 | cpu = new InstanceCPU(1156);
184 | } else if (type == 2) {
185 | cpu = new InstanceCPU(939);
186 | }
187 | return cpu;
188 | }
189 | }
190 | ```
191 |
192 | 创建主板工厂实现的示例代码如下:
193 |
194 | ```java
195 | /**
196 | * 创建主板的简单工厂
197 | */
198 | public class MotherboardFactory {
199 | /**
200 | * 创建主板接口对象的方法
201 | * @param type 选择主板类型的参数
202 | * @return 主板接口对象的方法
203 | */
204 | public static MotherboardApi createMotherboardApi(int type) {
205 | MotherboardApi motherboard = null;
206 | // 根据参数来选择并创建相应的主板对象
207 | if (type == 1) {
208 | motherboard = new AsusMotherboard(1156);
209 | } else if (type == 2) {
210 | motherboard = new MsiMotherboard(939);
211 | }
212 | return motherboard;
213 | }
214 | }
215 | ```
216 |
217 | (5) 下面看看装机工程师实现的代码示例如下:
218 |
219 | ```java
220 | /**
221 | * 装机工程师的类
222 | */
223 | public class ComputerEngineer {
224 | /**
225 | * 定义组装机器需要的 CPU
226 | */
227 | private CPUApi cpu = null;
228 |
229 | /**
230 | * 定义组装机器需要的主板
231 | */
232 | private MotherboardApi motherboard = null;
233 |
234 | /**
235 | * 装机过程
236 | * @param cpuType 客户选择所需 CPU 的类型
237 | * @param motherboardType 客户选择所需主板的类型
238 | */
239 | public void makeComputer(int cpuType, int motherboardType) {
240 | // 1: 首先准备好装机所需要的配件
241 | prepareHardwares(cpuType, motherboardType);
242 |
243 | // 2: 组装机器
244 | // 3: 测试机器
245 | // 4: 交付客户
246 | }
247 |
248 | /**
249 | * 装备装机所需要的配件
250 | * @param cpuType 客户选择所需 CPU 的类型
251 | * @param motherboardType 客户选择所需主板的类型
252 | */
253 | private void prepareHardwares(int cpuType, int motherboardType) {
254 | // 这里要去准备 CPU 和主板的具体实现,为了示例简单,这里只准备这两个
255 | // 可是,装机工程师并不知道如何去创建,怎么办呢?
256 |
257 | // 直接找相应的工厂获取
258 | this.cpu = CPUFactory.createCPUApi(cpuType);
259 | this.motherboard = MotherboardFactory.createMotherboardApi(motherboardType);
260 |
261 | // 测试一下配件是否好用
262 | this.cpu.calculate();
263 | this.motherboard.installCPU();
264 | }
265 | }
266 | ```
267 |
268 | (6) 看看此时的客户端,应该通过装机工程师来组装电脑,客户端需要告诉装机工程师他选择的配件。示例代码如下:
269 |
270 | ```java
271 | public class Client {
272 | public static void main(String[] args) {
273 | // 创建装机工程师对象
274 | ComputerENgineer engineer = new ComputerEngineer();
275 |
276 | // 告诉装机工程师自己选择的配件,让装机工程师组装电脑
277 | engineer.makeComputer(1, 1);
278 | }
279 | }
280 | ```
281 |
282 | 运行结构如下:
283 |
284 | ```
285 | now in Intel CPU, pins=1156
286 | now in AsusMotherboard, cpuHoles=1156
287 | ```
288 |
289 | ### 有何问题
290 |
291 | 看了上面的实现,会感觉很简单,通过使用简单工厂来获取需要的 CPU 和主板对象,然后就可以组装电脑了。有何问题呢?
292 |
293 | 上卖弄的实现,虽然通过「简单工厂」解决了:对于装机工程师,只知道 CPU 和主板的接口,而不知道具体实现的问题。「但还有一个问题没有解决,什么问题呢?那就是这些 CPU 对象和主板对象是有关系的,是需要互相匹配的。」而在上面的实现种,并「没有维护这种关联关系」,CPU 和主板是「由客户随意选择的」。这是有问题的。
294 |
295 | 比如在上面实现种的客户端,在调用 makeComputer 时,传入参数为 (1,2) 试试看,运行结果就会如下:
296 |
297 | ```
298 | now in Intel CPU, pins=1156
299 | now in MSIMotherboard, cpuHoles=939
300 | ```
301 |
302 | 观察上面的结果,就会看出问题。客户选择的 CPU 的针脚是 1156 针的,而选择的主板上的 CPU 插孔只有 939 针,根本「无法组装」。这就是「没有维护配件之间的关系造成的」。
303 |
304 | 该怎么解决这个问题呢?
305 |
306 | ## 解决方案
307 |
308 | ### 使用抽象工厂模式来解决问题
309 |
310 | 用来解决上述问题的一个合理的解决方案就是「抽象工厂模式」。那么什么是抽象工厂模式呢?
311 |
312 | #### 1. 抽象工厂模式的定义
313 |
314 | > 提供一个「创建」一系列或「互相依赖的」接口,而「无需指定」它们具体的类。
315 |
316 | #### 2. 应用抽象工厂模式来解决问题的思路
317 |
318 | 仔细分析上面的问题,其实有两个问题点,一个是「只知道所需要的一系列对象的接口」,而「不知道具体实现」,或者是「不知道具体使用哪一个实现」;另外一个是「这一系列对象是相关或相互依赖的」,也就是说「既要创建接口的对象」,还要「约束它们之间的关系」。
319 |
320 | 有朋友可能会想,「工厂方法模式」或者是「简单工厂」,不就可以解决「只知道接口而不知道实现的问题」吗?怎么这些问题又冒出来了呢?
321 |
322 | 注意:请注意,这里要解决的问题和工厂方法模式或简单工厂解决的问题「又很大不同的」,工厂方法模式或简单工厂「关注的是**单个**产品对象的创建」,比如创建 CPU 的工厂方法,它就只关心如何创建 CPU 的对象,而创建主板的工厂方法,及只关心如何创建主板对象。
323 |
324 | 这里要解决的问题是,要「创建一系列产品的对象」,而且「这一系列对象是构建新的对象所需要的组成部分」,也就是这一系列被创建的对象「相互之间是有约束的」。
325 |
326 | 解决这个问题的一个解决方案就是「抽象工厂方法」。在这个模式里面,会定义一个「抽象工厂」,这里面「虚拟地创建」客户端需要的这一系列对象,所谓「虚拟的」就是「定义创建这写对象的抽象方法」,并「不去真正地实现」,然后由具体的抽象工厂的「子类」来提供「这一列对象的创建」。这样一来就可以为同一个抽象工厂提供很多不同的实现,那么创建的这一系列对象就不一样了,也就是说,「抽象工厂在这里起到了一个**约束的作用**」,并「提供所有子类的一个**统一外观**」,来让客户端调用。
327 |
328 | ### 抽象工厂模式的结构和说明
329 |
330 | 【待补充】[P133]
331 |
332 | - Abstract Factory:抽象工厂,定义「创建一系列产品对象」的「操作接口」。
333 | - Concrete Factory:具体的工厂,实现「抽象工厂」定义的方法,具体「实现一系列产品对象」的「创建」。
334 | - Abstract Product:定义一类产品对象的接口。
335 | - Concrete Product:「具体的」产品「实现对象」,通常在具体的工厂里面,会选择具体的产品实现对象,来创建「符合抽象工厂定义的方法」返回的「产品类型的对象」。
336 | - Client:客户端,主要「使用抽象工厂」来获取一系列所需要的「产品对象」,然后向这些产品对象的接口编程,以实现需要的功能。
337 |
338 | ### 抽象工厂模式示例代码
339 |
340 | (1) 先看看抽象工厂的定义。示例代码如下:
341 |
342 | ```java
343 | /**
344 | * 抽象工厂的接口,声明创建抽象产品对象的操作
345 | */
346 | public interface AbstractFactory {
347 | /**
348 | * 示例方法,创建抽象产品 A 的对象
349 | * @return 抽象产品 A 的对象
350 | */
351 | public AbstractProductA createProductA();
352 |
353 | /**
354 | * 示例方法,创建抽象产品 B 的对象
355 | * @return 抽象产品 B 的对象
356 | */
357 | public AbstractProductB createProductB();
358 | }
359 | ```
360 |
361 | (2) 接下来看看产品的定义,由于只是示意,并没有去定义具体的方法,示例代码如下:
362 |
363 | ```java
364 | /**
365 | * 抽象产品 A 的接口
366 | */
367 | public interface AbstractProductA {
368 | // 定义抽象产品 A 的相关操作
369 | }
370 |
371 | /**
372 | * 抽象产品 B 的接口
373 | */
374 | public interface AbstractProductB {
375 | // 定义抽象产品 B 的相关操作
376 | }
377 | ```
378 |
379 | (3) 同样的,产品的各个实现对象也是空的。
380 |
381 | 实现产品 A 示例代码如下:
382 |
383 | ```java
384 | /**
385 | * 产品 A 的具体实现
386 | */
387 | public class ProductA1 implements AbstractProductA {
388 | // 实现产品 A 的接口中定义的操作
389 | }
390 |
391 | /**
392 | * 产品 A 的具体实现
393 | */
394 | public class ProductA2 implements AbstractProductA {
395 | // 实现产品 A 的接口中定义的操作
396 | }
397 | ```
398 |
399 | 实现产品 B 的示例代码如下:
400 |
401 | ```java
402 | /**
403 | * 产品 B 的具体实现
404 | */
405 | public class ProductB1 implements AbstractProductB {
406 | // 实现产品 B 的接口中定义的操作
407 | }
408 |
409 | /**
410 | * 产品 B 的具体实现
411 | */
412 | public class ProductB2 implements AbstractProductB {
413 | // 实现产品 B 的接口中定义的操作
414 | }
415 | ```
416 |
417 | (4) 再来看看「具体的工厂」的实现示意。具体的代码如下:
418 |
419 | ```java
420 | /**
421 | * 具体的工厂实现对象,实现创建具体的工厂对象的操作
422 | */
423 | public class ConcreteFactory1 implements AbstractFactory {
424 | public AbstractProductA createProductA() {
425 | return new ProductA1();
426 | }
427 |
428 | public AbstractProductB createProductB() {
429 | return new ProductB1();
430 | }
431 | }
432 |
433 | /**
434 | * 具体的工厂实现对象,实现创建具体的产品对象的操作
435 | */
436 | public class ConcreteFactory2 implements AbstractFactory {
437 | public AbstractProductA createProductA() {
438 | return new ProductA2();
439 | }
440 |
441 | public AbstractProductB createProductB() {
442 | return new ProductB2();
443 | }
444 | }
445 | ```
446 |
447 | (5) 实现客户端的代码示例如下:
448 |
449 | ```java
450 | public class Client {
451 | public static void main(String[] args) {
452 | // 创建抽象工厂对象
453 | AbstractFactory af = new ConcreteFactory1();
454 |
455 | // 通过抽象工厂来获取一系列的对象,如产品 A 和产品 B
456 | af.createProductA();
457 | af.createProductB();
458 | }
459 | }
460 | ```
461 |
462 | ### 使用抽象工厂模式重写示例
463 |
464 | 要使用抽象工厂模式来重写示例,先来看看如何使用抽象工厂模式来解决前面提示的问题。
465 |
466 | 装机工程师要组装电脑对象,需要「一系列的产品对象」,比如 CPU、主板等,于是创建一个「抽象工厂」给装机工程师使用,在这个抽象工厂「里面定义**抽象地**创建 CPU 和主板的方法」,这个「抽象工厂」就相当于一个「抽象的装机方案」,在这个装机方案里面,「各个配件都是能够相互匹配的」。
467 |
468 | 每个装机的客户,会提出他们自己的具体装机方案,或者是选择自己的装机方案,相当于为抽象工厂提供了具体的子类,在这些具体的装机方案类里面,会创建具体的 CPU 和主板实现对象。
469 |
470 | 此时系统的结构图如下所示:
471 |
472 | 【待补充】[P137]
473 |
474 | 虽然说是重写示例,但并不是前面写的都不要了,而是修改前面的示例,使它能更好地实现需要的功能。
475 |
476 | (1) 前面示例实现的「CPU 接口」和「CPU 实现对象」,还有「主板接口」和「主板实现对象」,都「不需要变化」,这里就不再赘述了。
477 |
478 | (2) 前面示例中创建 CPU 的「简单工厂」和创建主板的简单工厂,都「不再需要了」,直接删除即可,这里也就不去管它了。
479 |
480 | (3) 看看新加入的抽象工厂的定义。示例代码如下:
481 |
482 | ```java
483 | /**
484 | * 抽象工厂的接口,声明创建抽象工厂对象的操作
485 | */
486 | public interface AbstractFactory {
487 | /**
488 | * 创建 CPU 的对象
489 | * @return CPU 的对象
490 | */
491 | public CPUApi createCPUApi();
492 |
493 | /**
494 | * 创建主板的对象
495 | * @return 主板的对象
496 | */
497 | public MotherboardApi createMotherboardApi();
498 | }
499 | ```
500 |
501 | (4) 再看看「抽象工厂」的实现对象,也就是具体的「装机方案」对象。
502 |
503 | 先看看装机方法的一些实现。示例代码如下:
504 |
505 | ```java
506 | /**
507 | * 装机方案以:Intel 的 CPU + 华硕的主板
508 | * 这里创建 CPU 和主板对象的时候,是对应的,能匹配上的
509 | */
510 | public class Schema1 implements AbstractFactory {
511 | public CPUApi createCPUApi() {
512 | return new IntelCPU(1156);
513 | }
514 |
515 | public MotherboardApi createMotherboardApi() {
516 | return new AsusMotherBoard(1156);
517 | }
518 | }
519 | ```
520 |
521 | 再看看装机方案二的实现。示例代码如下:
522 |
523 | ```java
524 | /**
525 | * 装机方案二:AMD 的 CPU + 微星的主板
526 | * 这里创建 CPU 和主板对象的时候,是对应的,能匹配上的
527 | */
528 | public class Schema2 implements AbstractFactory {
529 | public CPUApi createCPUApi() {
530 | return new AMDCPU(939);
531 | }
532 |
533 | public MotherboardApi createMotherboardApi() {
534 | return new MSIMotherBoard(939);
535 | }
536 | }
537 | ```
538 |
539 | 评论:利用「抽象工厂」把两者 (CPU 和主板) 连接起来 (关联绑定),形成一个「产品簇」。简单工厂:选择实现;抽象工厂:选择产品簇的实现。
540 |
541 | (5) 下面来看看装机工程师类的实现。再现在的实现里面,装机工程师相当于「使用抽象工厂的客户端」,虽然是由「真正的客户端来选择和创建具体的工厂对象」,但是使用抽象工厂的是装机工程师对象。
542 |
543 | 装机工程师类跟前面的实现相比,主要的变化是:从客户端「不再传入选择 CPU 和主板的参数」,而是「直接传入客户选择并创建好的**装机方案对象**」。这样就「避免了单独去选择 CPU 和主板」,客户要选就是一套,就是一个系列。示例代码如下:
544 |
545 | ```java
546 | /**
547 | * 装机工程师的类
548 | */
549 | public class ComputerEngineer {
550 | /**
551 | * 定义组装电脑需要的 CPU
552 | */
553 | private CPUApi cpu = null;
554 |
555 | /**
556 | * 定义组装电脑需要的主板
557 | */
558 | private MotherboardApi motherboard = null;
559 |
560 | /**
561 | * 装机过程
562 | * @param schema 客户端选择的装机方案
563 | */
564 | public void makeComputer(AbstractFactory schema) { // ← 这里有点像 DoC/DI
565 | // 1: 首先准备好装机所需要的配件
566 | prepareHardwares(schema);
567 |
568 | // 2: 组装电脑
569 | // 3: 测试电脑
570 | // 4: 交付客户
571 | }
572 |
573 | /**
574 | * 准备装机所需要的配件
575 | * @param schema 客户选择的装机方案
576 | */
577 | private void prepareHardwares(AbstractFactory schema) {
578 | // 这里要去准备 CPU 和主板的具体实现,为了示例简单,这里只准备这两个
579 | // 可是,「装机工程师并不知道怎么去创建」,怎么办呢?
580 |
581 | // 使用抽象工厂来获取相应的接口对象
582 | this.cpu = schema.createCPUApi();
583 | this.motherboard = schema.createMotherboardApi();
584 |
585 | // 测试一下配件是否好用
586 | this.cpu.calculate();
587 | this.motherboard.installCPU();
588 | }
589 | }
590 | ```
591 |
592 | (6) 都定义好了,下面看看客户端如何使用抽象工厂。示例代码如下:
593 |
594 | ```java
595 | public class Client {
596 | public static void main(String[] args) {
597 | // 创建装机工程师对象
598 | ComputerEngineer engineer = new ComputerEngineer();
599 |
600 | // 客户选择并创建需要使用的装机方案对象
601 | AbstractFactory schema = new Schema1();
602 |
603 | // 告诉装机工程师自己选择的装机方案,让装机工程师组装电脑
604 | engineer.makeComputer(schema);
605 | }
606 | }
607 | ```
608 |
609 | 运行一下,测试看看,是否满足功能的要求。
610 |
611 | 如同前面的示例,定义了一个抽象工厂 AbstractFactory,在里面定义了创建 CPU 和主板对象的接口方法,但是在抽象工厂里面,「并没有指定具体的 CPU 和主板的实现」,也就是「无须指定它们具体的实现类」。
612 |
613 | CPU 和主板是相关的对象,是构成电脑的一系列相关配件,这个抽象工厂就相当于一个「装机方案」,客户端选择装机方案的时候,「一选就是一套」,CPU 和主板是确定好的,不让客户分开选择,这就「避免了出现不匹配的错误」。
614 |
615 | 评论:
616 |
617 | 可以把抽象工厂当成是菜单中的「一道菜」,这道菜由很多「原材料」组成,你可以选择菜单中的菜,选菜就是 new Schema (创建抽象工厂 API 的实例),然后把你要的菜传递给「厨师」,厨师看这个 Schema 里面要用些啥原材料 (原材料是啥在抽象工厂里面选择),然后做菜。
618 |
619 | emmm... 虽然似乎我这个例子不太好,因为原材料不是固定的种类,而上面那个配电脑是固定的 CPU 和主板 (只是品牌不同),固定的哪些配件形成一个「产品簇」,让用户去选择「产品簇」也就是 Schema,交给装电脑的人,它得到了 Scheme 无需关心兼容性问题,因为这是在「抽象工厂实现」中定好了的,装电脑的人它读取 scheme.createCPUApi() 和 scheme.createMotherboardApi() 得到 CPU 和主板两个配件的「固定类型」(CPUApi 和 MotherboardApi),而无需关心是什么品牌的 CPU 和主板,只管装上就好了。
620 |
621 | 转到:[抽象工厂模式 (下)](./6_抽象工厂模式_2.md)
--------------------------------------------------------------------------------
/4_单例模式_2.md:
--------------------------------------------------------------------------------
1 | ## 模式讲解
2 |
3 | ### 认识单例模式
4 |
5 | #### 1. 单例模式的功能
6 |
7 | 单例模式是用来保证这个类在运行期间「只会被创建一个实例」,另外,单例模式还提供了一个「全局唯一」访问这个类实例的访问点,就是 getInstance 方法。不管采用懒汉式,还是饿汉式的实现方式,这个全局访问点是一样的。
8 |
9 | 对于单例模式而言,不管采用何种实现方式,它都是「只关心实例的创建问题」,「并不关心具体的业务功能」。
10 |
11 | #### 2. 单例模式的范围
12 |
13 | 也就是多大的范围内是单例呢?
14 |
15 | 观察上面的实例可以知道,目前 Java 里面使用的单例是一个「虚拟机的范围」。因为装载类的功能是虚拟机,所以一个虚拟机在通过自己的 ClassLoader 装载饿汉式实例的单例类的时候会创建一个类的实例。(这里需要一些 Java 虚拟机的知识)
16 |
17 | 这就以为着,如果一个「虚拟机」里面有「多个 ClassLoader」,而且这些 ClassLoader 都装载某个类的话,这算是这个类的「单例」,它也会产生多个实例。当然,如果一个机器上有「多个虚拟机」,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,更不会是单例了。(上升到了 Java 虚拟机的范畴)
18 |
19 | #### 3. 单例模式的命名
20 |
21 | 注意:另外请注意一点,这里讨论的单例模式「不适用于集群环境」,对于集群环境下的单例这里不去讨论,它不属于这里的内容范畴。
22 |
23 | 一般建议单例模式的方法命名为 getInstance(),这个方法的返回类型肯定是单例类的类型了。getInstance() 方法「可以有参数」,这些参数可能是「创建类实例所需要的参数」,当然,大多数情况下是「不需要」的。
24 |
25 | 单例模式的名称有:单例、单件、单体等,只是翻译的不同,都是指的同一个模式。
26 |
27 | ### 懒汉式和饿汉式实现
28 |
29 | 前面提到了单例模式有两种典型的方案,一种叫「懒汉式」,另一种叫「饿汉式」,这两种方法究竟是如何实现的,下面分别来看看。
30 |
31 | 为了更清晰一点,只是实现基本的单例控制部分,不再提供示例的「属性和方法」了;而且暂时也不去考虑线「程安全的问题」,这个问题在后面会重点分析。
32 |
33 | #### 1. 第一种方案——懒汉式
34 |
35 | **(1) 私有化构造方法**
36 |
37 | 要想在运行期间控制某一个类的实例只有一个,首要任务就是「要控制创建实例的地方」,也就是「不能随随便便就可以创建类实例」,否则就「无法控制创建实例的个数」了。
38 |
39 | 现在是让使用类的地方创建实例,也就是在类外部来创建实例。
40 |
41 | 那么怎样才能「让类的外部不能创建一个实例」呢?很简单,「私有化构造方法」就可以了。示例代码如下:
42 |
43 | ```java
44 | private Singleton() {
45 |
46 | }
47 | ```
48 |
49 |
50 | **(2) 提供获取实例的方法**
51 |
52 | 构造方法被私有化了,外部使用这个类的地方不干了,外部创建不了类实例就没有办法调用这个对象的方法,就实现不了功能调用。这可不行,经过思考,单例模式决定让这个类提供一个方法来返回类的实例,方便外面使用。实例代码如下:
53 |
54 | ```java
55 | public Singleton getInstance() {
56 |
57 | }
58 | ```
59 |
60 | **(3) 把获取实例的方法变成静态的**
61 |
62 | 又有新的问题了,获取对象实例的这个方法就是一个实例方法,也就是说客户端想要调用这个方法,首先要先得到类实例,然后才可以调用。可是这个方法就是为了得到类实例,这样一来不就形成了一个「死循环」了吗?这也是典型的「先有鸡还是先有蛋问题」。
63 |
64 | 解决方法也很简单,「在方法上加上 static」,这样就可以直接通过类来调用这个方法,而不需要先得到类的实例。示例代码如下:
65 |
66 | ```java
67 | public static Singleton getInstance() {
68 |
69 | }
70 | ```
71 |
72 | **(4) 定义存储实例的属性**
73 |
74 | 方法定义好了,那么方法内部如何实现呢?如果直接创建实例并返回,这样行不行呢?实例代码如下:
75 |
76 | ```java
77 | public static Singleton getInstance() {
78 | return new Singleton();
79 | }
80 | ```
81 |
82 | 当然不行了,如果每次客户端访问这样直接 new 一个实例,那肯定会有多个实例,根本实现不了单例的功能。
83 |
84 | 怎么办呢?单例模式想到了一个方法,那就是「用一个属性来记录自己创建好的类实例」。当「第一次创建后,就把这个实例保存下来」,以后就可以「复用」这个实例,而不是重复创建对象实例了。示例代码如下:
85 |
86 | ```java
87 | private Singleton instance = null;
88 | ```
89 |
90 | **(5) 把这个属性也定义成静态的**
91 |
92 | 这个属性变量应该在什么地方用呢?肯定是「第一次创建类示例的地方」,也就是在前面那个「返回对象实例的静态方法里面使用」。
93 |
94 | 由于要在第一个静态方法里面使用,所以「这个属性被迫称为一个类变量」,要强制加上「static」,也就是说,这里并没有使用 static 的特性。示例代码如下:(编译器说了算,哈哈)
95 |
96 | ```java
97 | private static Singleton instance = null;
98 | ```
99 |
100 | **(6) 实现控制实例的创建**
101 |
102 | 现在应该到 getInstance 方法里面实现控制实例的创建了。控制方法很简单,只要「先判断一下是否已经创建过实例」就可以了。如何判断?那就看存放实例的属性是否有值,如果有值,说明已经创建过了,如果没有值,则应该创建一个。示例代码如下:
103 |
104 | ```java
105 | public static Singleton getInstance() {
106 | // 先判断 instance 是否有值
107 | if (instance == null) {
108 | // 如果没有值,说明还没有创建过实例,那就创建一个
109 | instance = new Singleton();
110 | }
111 | // 如果有值,或者是创建了值,那就直接使用
112 | return instance;
113 | }
114 | ```
115 |
116 | **(7) 完整的实现**
117 |
118 | 至此,成功解决了在运行期间,控制某个类只能被创建一个实例的要求。完整的代码如下。为了大家好理解,用注释标示了代码的先后顺序。
119 |
120 | ```java
121 | public class Singleton {
122 | // 4: 定义一个变量来存储创建好的类实例
123 | // 5: 因为这个变量要在静态方法中使用,所以需要加上 static 修饰
124 | private static Singleton instance = null;
125 |
126 | // 1: 私有化构造方法,好在内部控制创建实例的数目
127 | private Singleton() {
128 |
129 | }
130 |
131 | // 2: 定义一个发方法来为客户端提供类实例
132 | // 3: 这个方法需要定义成类方法,也就是要加 static
133 | public static Singleton getInstance() {
134 | // 6: 判断存储实例的变量是否有值
135 | if (instance == null) {
136 | // 6.1: 如果没有,就创建一个类的实例,并把赋值给存储类实例的变量
137 | instance = new Singleton();
138 | }
139 | // 6.2: 如果有值,那就直接使用
140 | return instance;
141 | }
142 | }
143 | ```
144 |
145 | #### 2. 第二种方案——饿汉式
146 |
147 | 这种方案和第一种方案相比,前面的私有化构造方法,提供静态的 getInstance 方法来返回实例等步骤都一样。差别就在于「如何实现 getInstance 方法」,在这个地方,单例模式还想到了「另外一种方法」来实现 getInstance 方法。
148 |
149 | 不就是要控制只创造一个实例吗?那么有没有什么现成的解决方法呢?很快,单例模式回忆起了 Java 中 static 特性。
150 |
151 | - static 变量「只在类装载的时候进行初始化」。
152 | - 多个实例 static 变量「会共享同一块内存区域」。
153 |
154 | 评论:饿汉式依赖于面向对象语言,如 Java 的 static 特性。
155 |
156 | 这就意味着,在 Java 中,static 变量「只会被初始化一次」,就是在类装载的时候,而且多个实例就会「共享这个内存空间」,这不就是单例模式要实现的功能吗?真是的来全不费功夫啊。根据这些知识,写出了第二种解决方案的代码。
157 |
158 | ```java
159 | public class Singleton {
160 | // 4: 定义一个静态变量来存储创建好的类实例
161 | // 直接在这里创建类实例,只能创建一次
162 | private static Singleton instance = new Singleton(); // 注意在这里就创建类实例了 (某些语言似乎不能这样干)
163 |
164 | // 1: 私有化构造方法,可以在内部控制创建实例的数目
165 | private Singleton() {
166 |
167 | }
168 |
169 | // 2: 定义一个方法来为客户端提供类实例
170 | // 3: 这个方法需要定义成类方法,也就是要加 static
171 | public static Singleton getInstance() {
172 | // 5: 直接使用已经创建好的实例
173 | return instance;
174 | }
175 | }
176 | ```
177 |
178 | 注意一下:这个方案用到了 static 的特性,而第一个方案却没有用到,因此两个方案步骤会有一些不同。在第一个方案里面,强制加上 static 也算是一步的,而在这个方案里面是主动加上 static,就不能单独算作一步了。
179 |
180 | 所以在查看上面两种方案代码的时候,仔细看看编号。顺着编号顺序来看,可以体会两种方案的不一样。
181 |
182 | 不管是采用哪一种方式,在运行期间,都「只会生成一个实例」,而「访问这些类的一个全局访问点」,就是那个「静态的 getInstance 方法」。
183 |
184 | #### 3. 单例模式的调用顺序示意图
185 |
186 | 由于单例模式有两种实现方式,所以它的调用顺序也分为两种。
187 |
188 | 先来看看懒汉式的调用顺序:
189 |
190 | 【待补充】[P86]
191 |
192 | 饿汉式的调用顺序如图:
193 |
194 | 【待补充】
195 |
196 | ### 延迟加载的思想
197 |
198 | 单例模式的「懒汉式」实现方式体现了「延迟加载的思想」。
199 |
200 | 什么是延迟加载呢?
201 |
202 | 通俗点说,延迟加载就是「一开始不要加载资源或数据」,一直等,「等到马上就要使用这个资源或者数据了」,躲不过去了才加载,所以也称「Lazy Load」,不是懒惰啊,是“延迟加载”,这在实际开发中也是「一种很常见的思想」,「尽可能地节约资源」。
203 |
204 | (As Lazy as possible.)
205 |
206 | 体现在什么地方呢?请看如下代码:
207 |
208 | ```java
209 | public static Singleton getInstance() {
210 | if (instance == null) {
211 | instance = new Singleton();
212 | }
213 | // ↑ 这里就体现了延迟加载,
214 | // 马上就要使用这个实例了,
215 | // 还不知道有没有呢,所以判断一下,
216 | // 如果没有,没办法了,赶紧创建一个吧。
217 | return instance;
218 | }
219 | ```
220 |
221 | ### 缓存的思想
222 |
223 | 单例模式的「懒汉式」实现还体现了「缓存的思想」,缓存也是实际开发中「常见的功能」。
224 |
225 | 简单讲就是,当某些资源或者数据「被频繁地使用」,而这些资源和数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都得从数据库或者硬盘上去获取,速度会很慢,将造成性能问题。(外部存储设备通常没有内部快,并且除了 RAM,还有 CPU 的三级缓存,速度更快。把外部存储的内容加载到更快的地方,实现高效 IO)
226 |
227 | 一个简单的解决方法就是:「把这些数据缓存到内存里面」,每次操作的时候,先到内存里面找,看看有没有这些数据,「如果有,就直接使用,如果没有就获取它,并设置到缓存中」,「下一次访问的时候就可以直接从内存中获取了」,从而节省大量时间。当然,「**缓存是一种典型的空间换时间的方案**」。「标星」
228 |
229 | 缓存在单例模式的实现中是怎样体现的呢?
230 |
231 | ```java
232 | public class Singleton {
233 | private static Singleton instance = null;
234 | // ↑ 这个属性是用来缓存实例的
235 |
236 | private Singleton() {
237 |
238 | }
239 |
240 | // ↓ 缓存的实现
241 | public static Singleton getInstance() {
242 | // 判断存储实例的变量是否有值
243 | if (instance == null) {
244 | // 如果没有,就创建一个类实例,并把值赋给存储类实例的变量
245 | instance = new Singleton();
246 | }
247 | // 如果有值,那就直接使用
248 | return instance;
249 | }
250 | }
251 | ```
252 |
253 | ### Java 中缓存的基本实现
254 |
255 | 下面来看看在 Java 开发中缓存的基本实现,在 Java 开发中「最常见的一种实现缓存的方式」就是「使用 Map」,基本步骤如下:
256 |
257 | (1) 先到缓存里面查找,看看是否存在需要使用的数据。
258 | (2) 如果没有找到,那么就「创建一个满足要求的数据」,然后把这个数据设置到缓存中,「以备下次使用」。如果找到了相应的数据,或者是创建了相应的数据,那就直接使用这个数据。
259 |
260 | 还是看看示例吧。示例代码如下:
261 |
262 | ```java
263 | /**
264 | * Java 中缓存的基本实现示例
265 | */
266 | public class JavaCache {
267 | /**
268 | * 缓存数据的容器,定义成 Map 是方便访问,直接根据 key 就可以获取 value 了
269 | * key 选用 String 是为了简单,方便演示
270 | */
271 | private Map map = new HashMap();
272 |
273 | /**
274 | * 从缓存中获取值
275 | * @param key 设置时候的 key 值
276 | * @return key 对应的 value 值
277 | */
278 | public Object getValue(String key) {
279 | // 先从缓存里面取值
280 | Object obj = map.get(key);
281 |
282 | // 判断缓存里面是否有值
283 | if (obj == null) {
284 | // 如果没有,那么就去获取相应的数据,比如读取数据库或文件
285 | // 这里只是演示,所以直接写个假的值
286 | obj = key+",value";
287 | // 把获取的值设置到缓存里面
288 | map.put()
289 | }
290 |
291 | // 如果有值了,就直接返回使用
292 | return obj;
293 | }
294 | }
295 | ```
296 |
297 | 这里只是缓存的基本实现,还有很多功能没有考虑,比如「缓存的清楚、缓存的同步等」。当然,Java 的缓存还有很多实现方式,也是非常复杂的,现在有很多专业的缓存框架。更多缓存的知识,这里就不再讨论了。
298 |
299 | 评论:
300 |
301 | 突然想到了之前 immersive-registry 项目中把缓存存到 redis 里面,是从 redis 里面查缓存是否存在,其实也可以适当将某些数据存到 golang 的 Slice 里面,从而获取更快的查询速度。但是全部交给 redis 有个好处,就是可以一定程度上避免内存爆炸的问题。
302 |
303 | ### 利用缓存来实现单例模式
304 |
305 | 应用 Java 缓存的知识,可以「变相实现 Singleton 模式」,也算是一个「模拟实现」把。只要创建一次对象实例后,就设置了缓存的值,那么下一次就不用再创建了。
306 |
307 | 虽然不是很标准的做法,但是同样可以实现单例模式的功能。为了简单,先不考虑多线程的问题,实例代码如下:
308 |
309 | ```java
310 | /**
311 | * 使用缓存来模拟实现单例模式
312 | */
313 | public class Singleton {
314 | /**
315 | * 定义一个默认的 key 值,用来标识再缓存中的存放
316 | */
317 | private final static String DEFAULT_KEY = "One";
318 |
319 | /**
320 | * 缓存实例的容器
321 | */
322 | private static Map map = new HashMap();
323 |
324 | /**
325 | * 私有化构造方法
326 | */
327 | private Singleton() {
328 | //
329 | }
330 |
331 | public static Singleton getInstance() {
332 | // 先从缓存中获取
333 | Singleton instance = (Singleton)map.get(DEFAULT_KEY);
334 | // 如果没有,就创建一个,让后设置回缓存中
335 | if (instance == null) {
336 | instance = new Singleton();
337 | map.put(DEFAULT_KEY, instance);
338 | }
339 | }
340 | }
341 | ```
342 |
343 | 是不是也能实现单例模式所要求的功能呢?前面讲过,「实现模式的方式有很多种,并不是只有模式的参考实现所实现的方式」,上面这种也能实现单例模式所要求的功能,只不过实现比较麻烦,不是太好而已,但再后面「扩展单例模式」的时候会用到。
344 |
345 | 另外,前面也讲过,「模式是经验的积累,模式的参考实现并不一定是最优的」,对于单例模式,后面将会给大家一些更好的实现方法。
346 |
347 | 评论:
348 |
349 | 看到最后,觉得作者说得很对!在开始看这一小节的时候,我思考既然有简单的方法,为什么要提出这个例子呢?所以,作者是想表明:设计模式的思想和具体的实现是分开的,具体实现有很多方式,参考实现并不一定是最佳实践,需要找到适合的。这里讲了 Java 的缓存知识,结合上面的例子拓宽视野,进一步感受到设计模式思想的实现可以高度自定义,从而满足特定的需求。
350 |
351 | ## 单例模式的优缺点
352 |
353 | ### 1. 时间和空间
354 |
355 | 比较上面两种写法:「**懒汉式是典型的时间换空间**」,也就是每次获取实例都会判断,看是否需要创建实例,「浪费判断的时间」。当然,如果一直没人使用的话,那就不会创建实例,则「节约内存空间」。
356 |
357 | 「**饿汉式是典型的空间换时间**」,当类装载的时候就会创建类实例,不管你用不用,先创建出来 (而占用了内存空间),然后每次调用的时候,就不需要在判断了,「节省了运行时间」。
358 |
359 | #### 2. 线程安全
360 |
361 | (1) 从线程安全性上讲,「**不加同步的懒汉式是线程不安全的**」,比如,有两个线程,一个是线程 A,一个是线程 B,它们同时调用 getInstance 方法,那就可能「导致并发问题」。
362 |
363 | 如下示例:
364 |
365 | ```java
366 | // ↓ B 线程运行到这句话,正在进行判断中
367 | public static Singleton getInstance() {
368 | if (instance == null) {
369 | // A 线程已经运行到这里了,还没有执行完下面一句话;
370 | // 而此时 B 线程运行到上面了,还在进行判断
371 | instance = new Singleton();
372 | }
373 |
374 | return instance;
375 | }
376 | ```
377 |
378 | 程序继续运行,两个线程都向前走了一步,如下:
379 |
380 | ```java
381 | public static Singleton getInstance() {
382 | if (instance == null) {
383 | // 1: 由于 B 线程运行较快,一下就判断出 instance == null,为 true
384 | // 2: 而此时 A 线程正在创建实例,也就是正运行 new Singleton()
385 | // 3: 但是 B 线程已经判断完了,也进入到这里了 (然后就重复创建实例了)
386 |
387 | instance = new Singleton(); // ← A 线程正在创建实例
388 |
389 | // 问题就产生了;这样就没有控制住,并发了,会创建两个实例了。
390 | }
391 |
392 | return instance;
393 | }
394 | ```
395 |
396 | 可能有些朋友会觉得文字描述不够直观,再来画个图说明一下,如图:
397 |
398 | 【待补充】[P91]
399 |
400 | 通过图中分解描述,明显看出,当 A、B 线程并发的情况下,会创建出两个实例来,也就是「单例的控制在并发的情况下失效了」。(单例控制,并发失效)
401 |
402 | (2)「**饿汉式是线程安全的**」,因为虚拟机保证「只会装载一次」,在装载类的时候是「不会发生并发的」。
403 |
404 | (3) 如何实现懒汉式的线程安全呢?
405 |
406 | 当然懒汉式也是「可以实现线程安全的」,只要加上「synchronized」即可,如下:
407 |
408 | ```java
409 | public static synchronized Singleton getInstance() { }
410 | ```
411 |
412 | 但是这样一来,「会降低整个访问的速度」,而且每次都要判断 (加上 synchronized 同步执行,每次执行到那个方法的时候,都会判断上次执行是否结束,来决定这次执行要不要继续 or 保持等待状态)。那么有没有「更好的方式」来实现呢?
413 |
414 | 评论:
415 |
416 | - 懒汉式因为在并发时会出现单例数量控制失效问题,所以是线程不安全的。
417 | - 而饿汉式由于不会在对象实例化时并发操作,所以是线程安全的。
418 | - 想要让懒汉式也变得线程安全,可以直接使用 synchronized 修饰符,但是效率会降低,更好的解决方式是使用下面的「双重检查加锁」。(好高级的名字对吧,哈哈)
419 |
420 | (4) 双重检查加锁
421 |
422 | 可以使用「**双重检查加锁**」的方式来实现,就可以「既实现线程安全,又能够使性能不受很大的影响」。那么什么是「双重检查加锁机制」呢?
423 |
424 | 所谓「双重检查加锁机制」,指的是:「并不是每一次」进入 getInstance 方法「都需要同步」,而是「先不同步」,进入方法过后,「先检查实例是否存在」,「如果不存在才进入下面的同步块」,这是「第一重检查」。
425 |
426 | 进入同步块过后,「再次检查实例是否存在」,如果不存在,就在「同步的情况下创建一个实例」,这是「第二重检查」。这样一来,就「只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间」。
427 |
428 | 评论:双重检查,判断实例是否需要创建不同步,而在创建实例的时候才实现同步操作。不会再像以前那样,一直会同步检测执行,如果下一次实例存在,直接返回已存在的实例。
429 |
430 | 思考:在 Golang 里面会是怎么样的?
431 |
432 | 双重检查加锁机制会「使用一个关键字 volatile」,它的意思是:被 volatile 修饰的变量的值,「将不会被本地线程缓存」,所有对该变量的读写都是「直接操作共享内存」,从而「确保多个线程能正确处理该变量」。(为了避免多线程操作同一个变量,由于虚拟机优化缓存而产生的潜在的问题)
433 |
434 | 注意:在 Java 1.4 及以前的版本中,很多 JVM 对于 volatile 关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制「只能用在 Java 5 及以上的版本」。
435 |
436 | 看看代码可能会更加清晰些。实例代码如下:
437 |
438 | ```java
439 | public class Singleton {
440 | /**
441 | * 对保存实例的变量添加 volatile 的修饰
442 | */
443 | private volatile static Singleton instance = null;
444 |
445 | private Singleton() {
446 |
447 | }
448 |
449 | public static Singleton getInstance() {
450 | // 先检查实例是否存在,如果不存在才进入下面的同步块
451 | if (instance == null) { // ← 相比于之前的方法,这里不再同步
452 | // 同步块,线程安全地创建实例
453 | synchronized (Singleton.class) {
454 | // 再次检查实例是否存在,如果不存在才真正地创建实例
455 | if (instance == null) {
456 | instance = new Singleton();
457 | }
458 | }
459 | }
460 |
461 | return instance;
462 | }
463 | }
464 | ```
465 |
466 | 这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。
467 |
468 | 它只在第一次创建实例的时候同步,以后就不需要再同步了,从而加快了运行速度。
469 |
470 | 提示:由于 volatile 关键字「可能会屏蔽掉虚拟机中的一些必要的代码优化」,所以「运行效率并不是很高」,因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用「双重检查加锁」机制来实现线程安全的单例,但「并不建议大量采用」,可以根据情况来选用。
471 |
472 | 评论:双重检查加锁机制也可以用在其他地方,解决线程安全的问题。
473 |
474 | ### 类级内部类(更好的方法)
475 |
476 | 在 Java 中一种更好的单例实现方式:利用类级内部类「标星」
477 |
478 | 根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,「既能够实现延迟加载,又能够实现线程安全」呢?
479 |
480 | 说明:还真有高人想到了这样的解决方案了,这个解决方案被称为「Lazy initialization holder class」,这个模式综合使用了 Java 的「类级内部类」和「多线程缺省同步锁」的知识,很巧妙地「同时实现了延迟加载和线程安全」。
481 |
482 | **1. 相应的基础知识**
483 |
484 | 先简单地看看「类级内部类」相关的知识。
485 |
486 | **什么是「类级内部类」?**
487 |
488 | 简单点说,类级内部类指的是:「有 static 修饰的成员式内部类」。如果「没有 static 修饰」的类级内部类被称为「对象级内部类」。
489 |
490 | 类级内部类相当于「其外部类的 static 成分」,它的「对象」与「外部类对象」之间「不存在依赖关系」,因此可以「直接创建」。而「对象级内部类」的实例,是「绑定在外部对象实例中的」。
491 |
492 | 类级内部类中,可以「定义静态的方法」。在静态方法中「只能够引用外部类中的静态成员方法或者成员变量」。
493 |
494 | 类级内部类相当于「其外部类的成员」,只有在「第一次被使用」的时候「才会被装载」。
495 |
496 | 评论:
497 |
498 | - 内部类就是 class 里面的 class
499 | - 类级:加 static 的内部 class
500 | - 对象级:不加 static 的内部 class (能被多次实例化对象)
501 | - 如果是「对象级内部类」,必须外部类被 new 了,才能再在其中 new 内部类。所以说是「绑定在外部对象实例中的」。
502 | - 类级内部类中的「静态方法」,只能够引用「外部类中的静态成员」。
503 | - 类级内部类只有在「第一次被使用的时候才会被装载」,和 JVM ClassLoader 的原理有关。
504 | - 对比:
505 | - 类级内部类:B 是 A 的类级内部类
506 | ```java
507 | class A {
508 | static class B { } // 有 static 修饰符,只会创建一个实例 (JVM ClassLoader 自动创建)
509 | }
510 | ```
511 | - 对象级内部类:B 是 A 的对象级内部类
512 | ```java
513 | class A {
514 | class B { } // 没有 static 修饰符,可以 new 多个 B 对象
515 | }
516 | ```
517 |
518 | 再来看看**多线程「缺省」同步锁**的知识。(缺省:就是 JVM 隐含执行同步操作)
519 |
520 | 大家都知道,在多线程开发中,为了解决并发问题,主要是通过 synchronized 来加「互斥锁」进行「同步控制」。但在某些情况下,「JVM 已经隐含地为您执行了同步」,这些情况下就不用自己来进行同步控制了。这些情况包括:
521 |
522 | - 由「静态初始化器」(在静态字段或 static{} 块中的初始化器) 初始化数据时
523 | - 访问 final 字段时
524 | - 在创建线程「之前」创建对象时
525 | - 线程「可以看见」它将要处理的对象时
526 |
527 | 评论:以上几种情况,概括讲:定义的对象当前的状态已经知道,已经确定的时候,JVM 会隐含地执行同步,而不用再手动加上互斥锁来手动同步了。可以利用这些特性,自然地实现一些需要同步,达到线程安全目的的效果。(JVM 来保证线程安全性)
528 |
529 | **2. 解决方案的思路**
530 |
531 | 要想很简单地实现线程安全,可以「采用静态初始化器的方法」,它可以由 JVM 来保证线程的安全性。比如前面的「饿汉式」实现方式 (利用静态字段)。但这样一来,不是「会浪费一定的空间」吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。
532 |
533 | 如果现在有一种方法能够「让类装载的时候不去初始化对象」,那不就解决问题了?一种可行的方法就是「采用类级内部类」,在这个「类级内部类里面去创建对象实例」。
534 |
535 | 这样一来,只要「不使用到」这个类级内部类,那就「不会创建对象实例」,从而「同时实现延迟加载和线程安全」。
536 |
537 | 看看代码示例可能会更清晰一些,示例代码如下:
538 |
539 | ```java
540 | public class Singleton {
541 | // ↓↓↓ 内部类 ↓↓↓ (class 里面的 class)
542 |
543 | /**
544 | * 类级内部类,也就是「静态的成员式内部类」,
545 | * 该「内部类」的实例与「外部类」的实例「没有绑定关系」,
546 | * 而且「只有被调用才会装载」,从而实现了「延迟加载」
547 | */
548 | private static class SingletonHolder {
549 | // ↑ 加 static 是属于「类级内部类」,JVM 只会创建一个实例 (在使用它时才会创建)
550 |
551 | /**
552 | * 静态初始化器,「由 JVM 来保证线程安全」
553 | */
554 | private static Singleton instance = new Singleton(); // ← 这句只有内部类被调用的时候才会执行,创建 Singleton 实例
555 | }
556 | // ↑↑↑ 内部类 ↑↑↑
557 |
558 | /**
559 | * 私有化构造方法
560 | */
561 | private Singleton() {
562 |
563 | }
564 |
565 | public static Singleton getInstance() {
566 | return SingletonHolder.instance;
567 | }
568 | }
569 | ```
570 |
571 | 仔细想想,是不是很巧妙呢!
572 |
573 | 当 getInstance 方法「第一次被调用」的时候,它「第一次读取 SingletonHolder.instance」,导致 SingletonHolder 类得到初始化;而这个类「在装载并被初始化的时候」,会「初始化」它的「静态域」,从而创建 Singleton 的实例,由于是「静态的域」,因此「只会在虚拟机装载类的时候初始化一次」,并「由虚拟机来保证它的线程安全性」。
574 |
575 | 这个模式的优势在于,「getInstance 方法并没有被同步」,并且「只是执行一个域的访问」,因此「延迟初始化并没有增加任何访问成本」。
576 |
577 | ## 单例和枚举(最佳方法)
578 |
579 | 按照《高效 Java 第二版》中的说法:「单元素的枚举类型」已经成为 Singleton 的最佳方法。
580 |
581 | 为了理解这个观点,先来了解一点相关的枚举知识,这里只是强化和总结一下枚举的一些重要观点,更多基本的枚举的使用,请参考 Java 编程入门资料。
582 |
583 | - Java 的枚举类型实质上是「功能齐全的类」,因此「可以有自己的属性和方法」。
584 | - Java 枚举类型的基本思想是「通过共有的静态 final 域」为「每个枚举常量」**导出**实例类。
585 | - 从某个角度讲,枚举是单例的「泛型化」,本质上是「单元素的枚举」。
586 |
587 | 用枚举来实现单例非常简单,只需要编写一个「包含单个元素的枚举类型」即可。实例如下:
588 |
589 | ```java
590 | /**
591 | * 使用枚举来实现单例模式的示例
592 | */
593 | public enum Singleton {
594 | /**
595 | * 定义一个枚举的元素,它代表了 Singleton 的一个实例
596 | */
597 | uniqueInstance;
598 |
599 | /**
600 | * 示意方法,单例可以有自己的操作
601 | */
602 | public void singletonOperation() {
603 | // 功能处理
604 | }
605 | }
606 | ```
607 |
608 | 使用枚举来实现单例控制会「更加简介」,而且「无偿地提供了序列化的机制」,并且由 JVM 从根本上提供保障,绝对防止多次实例化,是「更简、高效、安全的」实现单例的方式。
609 |
610 | 评论:枚举的方式直接用了静态 final 域,所以说是「无偿地提供了」。
611 |
612 | ## 思考单例模式
613 |
614 | ### 单例模式的本质
615 |
616 | > 单例模式的本质:控制实例数目。
617 |
618 | 单例模式是为了控制在运行期间,某些类的「实例数目只能有一个」。可能有人会思考,能不能控制实例数目为 2 个、3 个,或者是任意多个呢?目的都是一样的,「节约资源」啊,有些时候单个实例不能满足实际的需要,会忙不过来,根据测算,3 个实例刚刚好。也就是说,现在要「控制实例数目为 3 个」,怎么办呢?
619 |
620 | 其实思路很简单,就是利用上面「通过 Map 来缓存」实现单例的示例,进行变形,一个 Map 可以缓存任意多个实例。新的问题是,Map 中有多个实例,但是客户端调用的时候,到底返回哪一个实例呢,也就是「实例的调度问题」,我们只是想来展示设计模式,对于调度算法就不去深究了,做个简单的循环返回就可以了。实例代码如下:
621 |
622 | ```java
623 | /**
624 | * 简单演示如何扩展单例模式,控制实例的数目为 3 个
625 | */
626 | public class OneExtend {
627 | /**
628 | * 定义一个缺省 key 值的前缀
629 | */
630 | private final static String DEFAULT_PREKEY = "Cache";
631 |
632 | /**
633 | * 缓存实例的容器
634 | */
635 | private static Map map = new HashMap();
636 |
637 | /**
638 | * 用来记录「当前正在使用第几个」实例,到了控制的最大数目,就返回从 1 开始
639 | */
640 | private static int num = 1;
641 |
642 | /**
643 | * 定义控制实例的最大数目
644 | */
645 | private final static int NUM_MAX = 3;
646 |
647 | private OneExtend() {
648 |
649 | }
650 |
651 | public static OneExtend getInstance() {
652 | String key = DEFAULT_PREKEY+num;
653 |
654 | // ↓ 缓存的体现,
655 | // ↓ 通过控制缓存的数据多少,来控制实例数目
656 | OneExtend oneExtend = map.get(key);
657 | if (oneExtend == null) {
658 | oneExtend = new OneExtend();
659 | map.put(key, oneExtend);
660 | }
661 |
662 | // 把当前实例的序号加 1
663 | num++;
664 | if (num > NUM_MAX) {
665 | // 如果实例的序号「已经达到了最大数目了,那就从重复从 1 开始获取」
666 | num = 1;
667 | }
668 | return oneExtend;
669 | }
670 |
671 | public static void main(String[] args) {
672 | OneExtend t1 = getInstance();
673 | OneExtend t2 = getInstance();
674 | OneExtend t3 = getInstance();
675 | OneExtend t4 = getInstance();
676 | OneExtend t5 = getInstance();
677 | OneExtend t6 = getInstance();
678 |
679 | System.out.println("t1=="+t1);
680 | System.out.println("t2=="+t2);
681 | System.out.println("t3=="+t3);
682 | System.out.println("t4=="+t4);
683 | System.out.println("t5=="+t5);
684 | System.out.println("t6=="+t6);
685 | }
686 | }
687 | ```
688 |
689 | 测试一下,看看结果,如下:
690 |
691 | ```
692 | t1==OneExtend@1b083826
693 | t2==OneExtend@105fece7
694 | t3==OneExtend@3ec300f1
695 | t4==OneExtend@1b083826
696 | t5==OneExtend@105fece7
697 | t6==OneExtend@3ec300f1
698 | ```
699 |
700 | 第一个实例和第四个相同,第二个与第五个相同,第三个与第六个相同。也就是说一共有三个实例,而且调度算法是从第一个「依此」取到第三个,然后回来继续从第一个开始取到第三个。
701 |
702 | 当然在这里我们不去考虑复杂的调度情况,也不去考虑何时应该创建新实例的问题。
703 |
704 | 注意:这种实现方式同样是「线程不安全的」,需要处理,这里就不再展开去讲解了。
705 |
706 | ### 何时选用单例模式
707 |
708 | 建议在如下情况时,选用单例模式。
709 |
710 | 当「需要控制一个类的实例只有一个」,而且客户「只能从一个全局访问点访问它」时,可以选用单例模式,这些功能恰好是单例模式要解决的问题。
711 |
712 | ## 相关模式
713 |
714 | 很多模式都可以使用单例模式,只要这些模式中的某个类,需要「控制实例为一个」的时候,就可以「很自然地使用上」单例模式。
715 |
716 | 比如「抽象工厂方法」中的「具体工厂类」就通常是一个单例。
717 |
--------------------------------------------------------------------------------