创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建逻辑和使用逻辑。
1.1 概念
(1) 一个类只允许创建一个对象(实例),该类就是一个单例类。
(2) 常见的唯一性作用范围:进程。进程内唯一,进程间不唯一。
1.2 分类
饿汉式
(1) 在类加载的时候,instance静态实例就已经创建并初始化好。
(2) 创建过程线程安全,避免了用到时再初始化可能导致的性能问题。
(3) 不支持延迟加载,存在在不需要的时候占用资源的问题。
懒汉式
(1) 用到时再创建instance并初始化。
(2) 获取instance时加锁来保证线程安全,并发环境下频繁获取instance可能导致性能问题。
(3) 支持延迟加载。
双重检测
(1) 在懒汉模式的基础上,只有在instance未创建时,才进行加锁操作。
(2) 避免了懒汉式在并发情况下的性能问题。
(3) 支持延迟加载。
1.3 应用场景(通常从单个类的角度进行考虑)
(1) 需要解决资源访问冲突。
(2) 某些数据只应保存一份。
1.4 缺点
(1) 对于OOP中的抽象、继承、多态等特性支持不友好。
(2) 会隐藏类之间的依赖关系(需要从函数实现中去发现依赖关系)。
(3) 对代码的拓展性不友好。
(4) 对代码的可测试性不友好。
(5) 不支持带参数的构造函数。
1.5 代替方案
(1) 工厂模式。
(2) IOC容器。
2.1 概念
将对象(实例)的初始化操作封装在工厂类中,避免出现复杂冗长的初始化代码。
2.2 分类
简单工厂
同一种种类下,有多个不同类型的对象,单个对象的创建比较简单时,将多个对象的创建放在工厂类的函数中,对于多分支进行判断。
工厂方法
同一种种类下,有多个不同类型的对象,单个对象的创建比较复杂时,每个工厂类中只保留单个对象的创建,为工厂类再创建一个简单工厂,将对分支判断放在工厂的工厂中(可使用map来缓存特定工厂类的对象)。
抽象工厂
让一个工厂负责创建多个不同种类的对象。
2.3 应用场景(通常从一组类的角度进行考虑)
(1) 代码中存在分支判断,需要动态根据不同类型创建不同的对象。
(2) 单个对象本身的创建过程比较复杂。
2.4 作用
(1) 封装变化:创建逻辑对于调用者透明。
(2) 代码复用:创建逻辑抽离到独立的工厂类中后可以复用。
(3) 隔离复杂性,封装复杂的创建逻辑。
(4) 控制复杂性:将创建代码抽离,让原本的函数或类的职责更单一。
3.1 概念
先创建构建者,将一些必要的属性值通过set函数设置给构建者,然后使用构建者提供的build函数来创建真正的对象,在build函数中做一些参数校验等复杂逻辑操作。
3.2 应用场景(通常从单个类的角度进行考虑)
(1) 当类中属性过多。
(2) 类的属性之间有一定的依赖关系或约束条件的时候。
(3) 类的对象(实例)需要是不可变的。
3.3 作用
创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。
4.1 概念
使用已有对象进行复制来创建新对象。
4.2 应用场景
(1) 对象的创建成本比较大(对象的创建逻辑中涉及复杂计算、慢速IO操作)。
(2) 同一个类的不同对象之间差别不大。
结构型模式主要总结了一些类或对象组合在一起的经典结构,可以解决特定应用场景的问题。
1.1 概念
(1) 在不改变原始类代码的情况下,通过引入代理类来给原始类添加功能。
(2) 主要作用是控制访问。
1.2 实现方式
(1) 代理类与原始类实现相同的接口(协议),对于第三方或非本模块的类不适用。
(2) 代理类继承自原始类。
(3) 在运行时实现动态代理,使用Java的反射语法(将原始类的函数执行与代理类的函数执行进行绑定)、Objective-C的Runtime(方法交换)。
1.3 应用场景
(1) 业务系统中存在非功能性需求开发。
(2) 在RPC框架中的应用(远程代理,把网络通信、数据编解码等细节隐藏起来,客户端使用RPC服务的时候,就像使用本地函数一样,无需了解与服务器交互的细节)。
(3) 在缓存中的应用(针对需要支持缓存的接口,动态创建代理类来实现缓存功能)。
2.1 概念
(1) 将“抽象”(只包含骨架代码的类)和“实现”(具有真正业务逻辑的类)解耦,让他们可以独立变化。
(2) 若一个类存在多个独立变化的维度,通过组合的方式,让多个维度可以独立进行拓展,类似于“组合优于继承”的原则。
3.1 概念
(1) 通过组合来替代继承,主要作用是给原始类添加增强功能。
(2) 主要作用是解决继承关系过于复杂的问题。
3.2 特点
(1) 装饰器类和原始类继承自通用的父类,可以在原始类基础上实现多个平行的装饰器类。
(2) 装饰类体现了对于原始类功能的增强。
4.1 概念
一种事后的补救策略:将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
4.2 分类
类适配器
(1) 使用继承关系来实现。
(2) 不兼容的接口之间定义大部分相同,可发挥继承关系中复用代码的优势。
对象适配器
(1) 使用组合关系来实现。
(2) 不兼容的接口之间定义大部分不相同,组合结构比继承更加灵活。
4.3 应用场景
(1) 封装有缺陷的接口设计。
(2) 统一多个类的接口设计。
(3) 替换依赖的外部系统。
(4) 兼容老版本接口。
(5) 适配不同格式的数据。
5.1 概念
为子系统提供一组统一的接口,定义一组高层接口让子系统使用起来更方便。
5.2 应用场景
(1) 解决易用性问题。
(2) 解决性能问题。
(3) 解决分布式事务问题。
6.1 概念
将一组对象组织成树形结构,以表示一种“部分-整体”的层次结构,让开发者可以统一单个对象和组合对象的处理逻辑(对于业务场景的一种数据结构和算法的抽象)。
6.2 应用场景
(1) 文件系统(文件和文件夹的结构)。
(2) 业务中单个对象和组合对象都可看作树中的结点,可以统一处理逻辑,简化代码实现。
7.1 概念
(1) 若一个系统中存在大量重复的不可变对象,可以在内存中只保留一份实例,供多处代码引用。
(2) 主要作用是共享对象,节省内存。
7.2 实现方式
使用Map来存储已经创建过的享元对象,通过工厂模式对外提供对象。
7.3 应用场景
(1) 棋牌游戏(固定数量的棋子和牌面)。
(2) 文本编辑(文字格式)。
7.4 注意事项
(1) 与单例的区别:单例或多例在设计意图上是为了限制对象的个数;享元模式中一个类能创建多个对象,设计意图在于复用对象,节省内存。
(2) 与缓存的区别:日常所说的缓存更注重提升访问效率;而享元模式更注重复用对象。
(3) 与对象池的区别:对象池的“复用”可以理解为“重复使用”,主要目的是节省时间;享元模式中的“复用”可以理解为“共享使用”,主要目的是节省空间。
行为型模式主要解决类或对象之间的交互问题。
1.1 概念
在对象之间定义一个一对多的依赖,当一个对象(被观察者)状态改变时,所有依赖的对象(观察者)都会自动收到通知。
1.2 实现方式
(1) 同步阻塞
(2) 异步非阻塞:Google Guava EventBus(使用@Subscribe注解来定义每个Observer能够接收的消息类型)。
(3) 进程内通知
(4) 跨进程通知:消息队列。
2.1 概念
在方法中定义业务逻辑的骨架,将一些步骤推迟到子类中实现,可以让子类在不改变整体业务逻辑的情况下,修改一些步骤的实现。
2.2 作用
(1) 复用:把业务逻辑中不变的部分抽象到父类中,可变的部分留给子类实现,子类可以复用父类的模板方法中定义的业务流程。
(2) 扩展:框架通过模板模式提供功能扩展点,让用户可以在不修改框架源码的情况下,基于扩展点定制框架功能。
2.3 与同步回调的区别
(1) 模板模式通过继承来实现,表现的是父类和子类之间的关系。
(2) 同步回调通过组合来实现,表现的是不同对象之间的关系。
典型的基于接口(协议)而非实现编程和工厂模式的体现。
3.1 概念
定义一系列算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
3.2 作用
将策略的定义、创建和使用进行解耦。
3.3 实现方式
(1) 策略的定义:使用接口(协议)进行抽象。
(2) 策略的创建:使用工厂类创建策略对象。
(3) 策略的使用:在运行时动态确定使用哪种策略。
4.1 概念
将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求(直到链上的某个接收对象能够处理它为止;也可以让所有接收对象都处理请求)。
4.2 作用
职责链模式常用在框架开发中,用来实现框架的过滤器、拦截器功能,让框架的使用者在不需要修改框架源码的情况下,添加新的过滤拦截功能。这也体现了之前讲到的对扩展开放、对修改关闭的设计原则。
4.3 实现方式
(1) 使用接收对象链来保存所有接收对象(可用链表,可用数组)。
(2) 在抽象父类(使用链表存储时)或对象链(使用数组存储时)中进行链式调用。
5.1 概念
(1) 状态机:包含状态、事件和执行这3个组成部分,事件会触发状态的转移及动作的执行。
(2) 状态模式是用来实现状态机的一种方式,而状态机常用在游戏、工作流引擎等系统开发中。
5.2 实现方式
对于状态不多且状态切换简单,但是事件执行逻辑复杂的场景,将状态的事件执行抽象成接口(协议),状态机和状态双向依赖,有事件执行时,状态机调用状态对象的方法,状态对象会改变状态机中保存的相关内容。
6.1 概念
将集合对象(数组、链表、树、图)的遍历操作从集合类代码中拆分出来,放到迭代器类中。
6.2 作用
(1) 针对遍历起来复杂的数据结构(树、图),将复杂的遍历操作封装到迭代器中,降低了容器类代码的复杂程度,使两者的职责更单一,也方便调用者使用。
(2) 可以使用多个迭代器遍历同一个容器,并且互相独立不受影响。
(3) 添加新的遍历算法更加容易,更符合开闭原则。迭代器都实现自相同的接口(协议),替换迭代器也更加容易。
较难理解和使用,不到万不得已不建议使用。
7.1 概念
允许一个或多个操作应用到一组对象上,将对象和操作进行接耦。
7.2 应用场景
访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile),这些对象继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等)。为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及频繁的代码修改,使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
8.1 概念
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
8.2 应用场景
主要是用来防丢失、撤销、恢复。
9.1 概念
将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
9.2 应用场景
命令模式常用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。
10.1 概念
为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
10.2 应用场景
(1) 编译器。
(2) 规则引擎。
(3) 正则表达式。
11.1 概念
定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
11.2 与观察者模式的区别
(1) 在观察者模式的应用场景中,参与者之间的交互一般都是单向的,一个参与者只有一个身份,要么是观察者,要么是被观察者。
(2) 在中介模式的应用场景中,参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。