├── .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 | ![](img/adapter-two-way.png) 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 | ![](img/adapter-class.png) 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 | ![](img/adapter-too-many.jpg) 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 | ![](img/adapter-cannot-directly-use.png) 253 | 254 | 这就意味着,想要同时支持文件和数据库存储两种方式,需要「额外」地做一些工作,才可以让第一版的实现适应新的业务需要。 255 | 256 | 有朋友可能会想,干脆按照第二版的接口要求来「重新实现一个」文件操作的对象不久可以了吗,这样做确实可以,但是何必要重新做已经完成的功能呢?应该想办法复用,而不是重新实现。 257 | 258 | 一种很容易想到的方式是「直接修改已有的第一版代码」。但这种方式是不太好的,如果直接修改第一版的代码,那么久可能会导致「其他依赖于这些实现的应用不能正常运行」,再说,有可能第一版和第二版的开发公司是不一样的,在第二版实现的时候,根本拿不到第一版的源代码。 259 | 260 | ![](img/adapter-modify-impl-cause-problem.png) 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 | ![](img/adapter-structure.png) 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 | ![](img/adapter-example.png) 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 | ![](img/facade.png) 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 | ![](img/facade-as-interface.png) 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 | ![](img/factory-method-framework.png) 54 | 55 | 如果没有框架,那么客户要求的所有功能都由开发者自己来开发,没问题,「同样可以」实现用户需要的功能,只是开发人员的「工作多点」。 56 | 57 | 如果有了框架,框架本身完成了一定的功能,那么框架已有的功能开发人员就可以不做了,开发人员只需要完成框架没有的功能,最后同样是完成客户端要求的所有功能,但开发者的「工作就减少了」。(自己开发框架:高度自定义) 58 | 59 | 也就是说,基于框架来开发,软件要完成的功能并没有变化,还是客户要求的所有功能,也就是 “事情还是那些事情” 的意思。但是有了框架后,「框架完成了一部分功能」,然后开着再完成一部分功能,最后由「框架和开发人员合起来」完成了整个软件的功能,也就是「看这些功能 “由谁做” 的问题」。 60 | 61 | **基于框架开发,可以「不去做框架所做的事情」,但是「应该明白框架在干什么」,以及「框架是如何实现相应功能的」。** 62 | 63 | 事实上,在实际开发中,应用程序和框架的关系「通常都不会像」上面讲述的那样,「分得那么清楚」,更多普遍的是「相互交互」的。也就是应用程序做一部分工作,框架做另一部分工作,然后应用程序再做一部分工作,框架再做另一部分工作。「如此交错」,最后由应用程序和框架组合起来完成用户的功能需求。 64 | 65 | 也用个示意图来说明,如图: 66 | 67 | ![](img/factory-method-framework-x-app.png) 68 | 69 | 如果把这个由应用程序和框架「组合再一起构成」的矩形,当作最后完成的软件。试想一下,如果你「不懂框架」在干什么,相当于框架对你来讲「是个黑河」,也就是相当于在上图中去掉框架的两块,会发现什么?没错,剩下的应用程序是「支离破碎」的,是「相互分隔开来的」。 70 | 71 | ![](img/factory-method-framework-x-app-2.png) 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 | ![](img/factory-method-structure.png) 154 | 155 | ![](img/factory-method-structure-2.png) 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 | ![](img/factory-method-client-call-1.png) 464 | 465 | 接下来看看客户端「使用 Creator 对象」时候的调用顺序示意图,如图: 466 | 467 | ![](img/factory-method-client-call-2.png) 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 | ![](img/factory-method-ac-normal.png) 35 | 36 | 当有了 IoC/DI 容器后,A 类「不再主动去创建 C 了」,如图: 37 | 38 | ![](img/factory-method-ac-unlink.png) 39 | 40 | 而是「被动等待」,等待 IoC/DI 的容器获取一个 C 的实例,然后「反向地注入到 A 类中」,如图: 41 | 42 | ![](img/factory-method-ac-ioc-di.png) 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 | ![](img/factory-method-parallel-class.png) 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 | --------------------------------------------------------------------------------