来自极客时间 王争设计模式之美 专栏总结
设计模式要干的事情就是解耦
- 创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚低耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性
- 观察者模式
- 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知 -> 被依赖的对象叫作被观察者,依赖的对象叫作观察者
- 举个例子 -> 用户触发一个注册事件 需要发放体验金和优惠券同时还要发送一封站内信 -> 注册事件就是被观察者 发放体验金等行为就是观察者
- 具有同步阻塞的实现方式,也有异步非阻塞的实现方式,有进程内的实现方式,也有跨进程的实现方式 -> 同步阻塞的实现方式就是观察者和被观察者代码在同一个线程中执行,被观察者一直阻塞,直到所有的观察者代码都执行完成,才执行后续的代码 -> 将观察者的代码放在另外一个新创建的线程执行代码
- 引入消息队列 -> 被观察者完全不感知观察者,观察者完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑
- Guava EventBus -> 定义了一个 Observer 注册表,记录了消息类型和可接收消息函数的对应关系 -> 当调用 register() 函数注册观察者的时候,EventBus 通过解析 @Subscribe 注解,生成 Observer 注册表。当调用 post() 函数发送消息的时候,EventBus 通过注册表找到相应的可接收消息的函数,然后通过 java 的反射语法来动态创建对象、执行函数。
- 模板模式
- 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤 -> 定义接口 提供抽象 对部分方法留给子类实现 相当于实现一个钩子函数
- 复用 -> 把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1() method2() 等留给子类来实现 -> 所有的子类都可以复用父类中模板方法定义的流程代码
- 扩展 -> 对于框架来说增加了可扩展性 -> 让框架用户在不修改框架源码的情况下,定制化框架的功能
- 回调 -> 相对于普通的函数来说, 回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是 “回调函数”。-> 回调函数 + 依赖注入 + 组合 可以实现定制化一些函数的调用 -> 回调分为 同步回调 和 异步回调 同步回调是在 B 类的 P 函数返回之前调用 F 函数 异步回调是在 B 类的 P 函数调用结束之后 再将结果通过回调接口返回给用户
- 策略模式
- 策略的定义->包含一个策略接口和一组实现这个接口的策略类
- 策略的创建->因为策略会包含一组策略,在使用它们时,一般会通过类型来判断创建那个策略来使用。同时为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。我们这个时候就可以将创建的策略逻辑抽离出来放到工厂类中
- 如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,这个时候就可以缓存创建的过程
- 如果策略是有状态的,则需要使用 if 判断来实现
- 策略的使用->在代码动态运行时通过 type 来找到对应的实现方法从而使用相应的策略
- 利用策略模式避免分支判断->得益于策略工厂类同时更本质上来讲就是借助 “查表法”。
- 在策略工厂中既可以保存 class 类 也可以保存创建成功的对象
- 在工厂 我们通过反射来完成对象的创建->这个反射创建需要具有那些类是可以通过配置文件、自定义的注解、静态方法等等来完成的
- 例子
- 排序一个文件(内存8G)->文件10G则可以使用外部排序算法->文件100G则利用 CPU 多核的优势,在外部排序的基础之上进行优化,加入多线程并发排序的功能->文件1TB,使用 MapReduce 框架,利用多机的能力提高排序的效率
- 根据不同的文件大小应用不同的策略->使用策略工厂即可
- 责任链模式
- 将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的所有对象都处理完或者在得到了一个正确处理之后即可退出当前的责任链
- 责任链可以避免一连串的 if 连续调用同时能够满足对扩展开放 对修改关闭的问题,只需删除对应的处理逻辑即可 不需要陷入到对应的代码实现中去修改代码
- 例子
- 现实对 HTTP 请求的过滤功能,比如 鉴权、限流、记录日志、验证参数 等
- 状态机模式
- 状态机有 3 个组成部分: 状态(State) 事件(Event) 动作(Action) -> 事件也称为转移条件(Transition Condition) -> 事件触发状态的转移及动作的执行
- 实现方式
- 分支逻辑法
- 查表法->定义一份表格,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到新状态及其执行的动作
- 状态模式
- 状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,避免分支判断逻辑
- 同时在状态机当中保存一份当前的状态
- 迭代器模式
- 迭代器模式主要用于遍历集合对象->迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一
- 迭代器中需要定义 hasNext() currentItem() next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器
- 对于链表等遍历方式比较简单的无需使用迭代器模式但对于(树 图)来说,存在各种复杂的遍历方式。比如树的前中后序遍历、按层遍历等->将遍历操作拆分到迭代器类中
- 通过迭代器的游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响
- 遍历时不可以增删元素 如果在遍历的过程中增删元素 java 的做法就是直接报错
- 遍历容器时创建快照的方法 -> 为每个元素保存两个时间戳 一个是添加时间戳 addTimestamp 一个是删除时间戳 delTimestamp 当增加元素时 addTimestamp = currentTimestamp, delTimestamp = Long.MAX_VALUE 当删除元素时 delTimestamp = currentTimestamp -> 记录遍历容器时的时间戳 snapshotTimestamp -> 在最终遍历时只有 addTimestamp < snapshotTimestamp < delTimestamp 的元素才是真正属于当前迭代器的快照
- 访问者模式(允许一个或者多个操作应用到一组对象上,解耦操作和对象本身)
public abstract class AbstractTest { abstract public void accept(Visit visit); } public class A extends AbstractTest { public void accept(Visit visit) {visit.exe(this);} } public class B extends AbstractTest { public void accept(Visit visit) {visit.exe(this);} } public interface Visit { public void exe(A a) public void exe(B b) }
- 通过将所有的对象的操作从业务操作抽离出来,定义在独立细分的访问者类中 Visit 中
- Single Dispatch -> 执行那个对象的方法,根据对象的运行时类型来决定;执行对象的那个方法,根据方法参数的编译时类型决定 | Double Dispatch -> 执行那个对象的方法,根据对象运行时类型来决定;执行对象的那个方法,根据方法参数的运行时类型来决定 -> Dispatch 可以理解为消息传递,一个对象调用另一个对象的方法,就相当于给它发送一条消息。这条消息起码要包含对象名、方法名、方法参数 -> Single 表示执行那个 对象 的那个方法,只跟 对象 的运行时类型有关。Double 执行那个对象的那个方法,跟 对象 和 方法参数 两者的运行时类型有关。
- 如果对于一个类的操作方法特别的多,则更加推荐使用访问者模式。否则只有几个方法时使用工厂模式相对而言更加便于理解
- 备忘录模式(在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态)
- 通过栈的方式 每当需要保存当前的记录时 直接放入栈中即可
- 当需要备忘的记录过于大的时候 我可以不用每次都记录对应的备份文件 而是记录每一次所做的操作信息从而减少全量备份的数量和频率
- 命令模式(将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制))功能
- 命令模式用到最核心的实现手段,是将函数封装成对象。在C语言中支持函数指针,我们可以把函数当作变量传递。但是当函数无法作为参数传递时,我们可以将函数封装成对象。设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。
- 常用的一种实现思路是利用多线程。一个线程接收请求,接收到请求之后,启动一个新的线程来处理请求。
- 一个线程内轮询接收请求和处理请求。
- 策略模式侧重于一个特定的应用场景,用来解决根据运行时状态从一组策略中选择不同策略的问题 而 工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以时策略,但也可以是其他东西 命令模式,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换。
- 解释器模式(为某个语言定义它的语法表示,并定义一个解释器用来处理这个语法)
- 自定义告警规则(|| && > == )等等的规则 用特定的解释器去解释
- 中介者模式(中介模式定义了一个单独的 (中介) 对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互)
- 引入中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低代码的复杂度。
- 观察者增加一个中间层可以实现一些解耦
- 代理模式(在不改变原始类(或者叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能)
- 两种实现方式 -> 代理类和被代理类都实现同一个接口 | 通过继承的方式让代理类继承原始类,然后扩展功能
- 动态代理 -> 不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类
- 桥梁模式(将抽象和实现解耦,让他们独立变化)
- 一个类存在两个(或多个)独立变化的维度, 我们通过组合的方式, 让这两(或多个)维度可以独立进行扩展 -> 通过组合代替继承关系,避免继承层次的指数级爆炸
- 此时的抽象和接口并非抽象类或者接口->JDBC本身就相当于 “抽象” 抽象出一套跟具体的数据库无关的、被抽象出来的一套 “类库”。具体的 Driver (com.mysql.jdbc.Driver) 就相当于 “实现” 指的就是跟具体数据库相关的一套 “类库”。-> 通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作, 最终都委托给 Driver 执行。
- 桥接就是面向接口编程的集大成者。面向接口编程只是说在系统的某一个功能上将接口和实现解耦,而桥接是详细的分析系统功能,将各个独立的维度都抽象出来,使用时按需组合
- 举个很简单的例子,现在有两个纬度 Car 车 (奔驰、宝马、奥迪等) Transmission 档位类型 (自动挡、手动挡、手自一体等) 按照继承的设计模式,Car是一个Abstract基类,假设有M个车品牌,N个档位一共要写MN个类去描述所有车和档位的结合。 而当我们使用桥接模式的话,我首先new一个具体的Car(如奔驰),再new一个具体的Transmission(比如自动档)。然后奔驰.set(手动档)就可以了。 那么这种模式只有M+N个类就可以描述所有类型,这就是MN的继承类爆炸简化成了M+N组合。
- 装饰器模式(使用组合来替代继承)
- 通过将 继承关系 改为 组合关系 -> 装饰器类和原始类继承同样的父类,这样我们可以对原始类 “嵌套” 多个装饰器类
- 装饰器是对功能的增强,这也是装饰器模式应用场景的一个重要特点
- 适配器模式(将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作 Adaptee(被适配器) Adapter(适配器) ITarget(目标方法) )
- 两种实现方式: 类适配器和对象适配器 -> 类适配器使用继承关系来实现,对象适配器使用组合关系来实现
- 类适配器直接继承自 Adaptee 复用父类的接口,同时 Adaptee 和 ITarget 的接口定义大部分都相同,这样就可以减少重复的代码量
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分不相同,则使用对象适配器,将 Adaptee 关联到 Adapter 中 因为组合结构相对于继承更加灵活
- 5个场景 -> 封装有缺陷的接口设计 | 统一多个类的接口设计 | 替换依赖的外部系统 | 兼容老版本的接口 | 适配不同格式的数据
- 代理模式 桥接模式 装饰器模式 适配器模式
- 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同
- 桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变 -> 就是将所有的对象都关联在一起
- 装饰器模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用 (继承 关联都可以)
- 适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是原始类相同的接口
- 门面模式
- 门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用
- 系统A 提供了 a b c d 四个接口。系统B 为完成业务功能,需要调用 a b d 接口。利用门面模式 提供一个包裹 a b d 接口调用的门面接口 x 给系统B 使用
- 解决易用性问题 -> 用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口
- 解决性能问题-> 将多个接口调用替换为一个门面接口调用,减少网络通信成本
- 解决分布式事务问题 -> 将多个不同的对数据库事物的操作 放到一个接口中进行
- 组合模式
- 将一组对象组织(Compost)成树形结构,以表示一种 “部分 - 整体” 的层次结构。组合让客户端(代码的使用者)可以统一单个对象和组合对象的处理逻辑
- 在统计文件和文件目录的业务场景 文件和文件目录都属于文件系统 文件系统和文件及文件目录之间就是 整体-部分 的关系
- 享元模式
- 当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多出代码引用。这样可以减少内存中对象的数量,起到节省内存的目的
- 对于一些相似的对象,我们也可以将对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元
- 享元中 “不可变对象” 指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性) 就不会再被修改。
- 通常通过工厂来缓存已经创建过的享元对象
- 工厂模式
- 工厂模式 Client 只需要负责获取对象就行了 工厂自己负责对象的创建 -> 工厂变成抽象则对应的对象也要抽象一下 -> 当然产品对象本身就是一个抽象则更加通用
- 复杂度无法被消除,只能被转移:
- 不用工厂模式,if-else 逻辑、创建逻辑和业务代码耦合在一起
- 简单工厂是将不同创建逻辑放到一个工厂类中,if-else 逻辑在这个工厂类中
- 工厂方法是将不同创建逻辑放到不同工厂类中,先用一个工厂类的工厂来来得到某个工厂,再用这个工厂来创建,if-else 逻辑在工厂类的工厂中
- 建造者模式
- 建造者需要 Client 自己提供原料 但是不管这个原料是怎么加工的 封装了构建者对对象的加工方法 一定存在加工的方法 -> 建造者抽象一下 则 对应的 对象也要抽象一下 -> 解耦则需要来加一层 来一个抽象 -> 只要需求一变 就加一个中间层
- 建造者用来创建一种类型的复杂对象,通过设置不同的可选参数,”定制化” 地创建不同的对象
- 原型模式
- 对象中一些数据需要经过复杂的计算才能得到或者需要通过 IO 才能得到,这个时候就可以通过原型模式来拷贝一个得到。
- 浅拷贝和深拷贝的区别在于 -> 浅拷贝只会复制图中的索引(散列表), 不会复制数据(对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身 -> 浅拷贝得到的对象和原始对象共享数据(对象) | 而深拷贝得到的是一份完完全全独立的对象。
- 深拷贝的实现方式 -> 递归拷贝对象 | 序列化和反序列化对象
- 对于HashMap这种的 可以先浅拷贝一个 NewKeyword 当我们需要更新对应的 Data 时,我们可以通过深度拷贝的方式创建一份新的对象,替换 NewKeyword 中的老对象