摘要:结构型:代理、装饰器、适配器、桥接、外观、享元和组合
参考:
《设计模式之美》
JavaScript 设计模式核⼼原理与应⽤实践
Java设计模式:23种设计模式全面解析(超级详细)
JavaScript设计模式es6(23种)
23种设计模式——创建型设计模式(5种)
设计模式:可复用面向对象软件的基础
等等
全文中的组合关系即关联关系
代理模式
代理模式主要原理是在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用,一般情况下代理类和原始类的接口是一致的。同时,如果要扩展功能,可以通过修改代理类来完成,这样符合开闭原则。如果原始类是第三方提供的、无法修改的,可以通过继承来扩展类的功能。前端业务中最常见的有四种代理:事件代理、虚拟代理、缓存代理和保护代理。
通用实现流程:
1、确定原始类需要代理的接口
2、创建代理类,重写原始类需要代理的接口,并添加条件限制
3、在业务代码中使用代理类实例对象替换原始类对象
示例
1、事件代理,最常见的就是给父元素绑定事件,点击子元素时会有不同提示。
1 | // DOM |
2、虚拟代理,图片预加载时使用虚拟代理便可以将预加载图片和操作 DOM 的工作(img 节点的初始化和后续的改变)分开。
1 | // 操作 DOM 类 |
3、缓存代理,就是通过存储来加速调用,以空间换时间的理念,缓存特定的参数获取到的结果,当下次相同参数调用该方法,直接从缓存中返回数据。
1 | // 求和 |
4、保护代理,就是在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。ES6 中的 Proxy 就是为拦截而生的,实现保护代理的首选方案就是它。就以非 VIP 用户禁止查看全文为例
1 | const restrictedInfo = ["comment", "allContent"] |
装饰器模式
装饰器模式是指在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象,一般情况下接口也是一致的。装饰器模式跟代理模式有些相似,都创建了一个新的对象来间接管理原有对象,但两者注重点不一样:代理模式是给一个对象提供一个代理对象,并由代理对象来限制原有对象的使用而非加强功能;装饰器模式强调的是增强原有对象功能,使用装饰器后的新对象可以直接替换原有的对象。
通用实现流程:
1、确定原始类需要包装的接口
2、创建装饰器类,重写原始类需要包装的接口,并添加增强的功能
3、在业务代码中使用装饰器实例对象替换原始类对象
示例
给所有的确认按钮添加点击后变为不可点击的效果
1 | // DOM |
适配器模式
适配器模式是将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。适配器模式是一种补救策略,适配器提供跟原始类不同的接口。适配器模式和装饰器模式的主要区别在于两个模式设计的目的是不同的,适配器模式是通过改变接口来达到重复使用原来接口的目的,且接口名称是不一致的,以便区分各个适配器。而装饰器模式是为了增强原有对象的方法,且增强后方法名要和原有对象保持一致,以确保使用方法不变。简单来说,装饰器模式是已经有了功能 A,但需要新增功能 B,功能 A 是可以工作但不满足新需求。适配器模式是已经有了满足需求的功能 A,但需要满足条件 B 才能正常调用,适配器做的工作就是创建条件 B,然后使用功能 A,B 是 A 的前置条件。适配器模式分为类结构型模式和对象结构型模式两种:类结构型模式采用继承机制来组织接口和类,适配器类是继承原始类,适配器这个子类新增一个满足条件 B 的方法,然后调用父类原始类的 A 功能接口;对象结构型模式釆用组合(聚合)来组合对象,创建一个新的适配器类,适配器实例的一个属性是原始类的实例,然后在该适配器类中新增一个满足条件 B 的方法,然后再调用成员变量的 A 功能接口。
适配器模式的主要应用场景是“接口不兼容”:
1、版本升级后,用于兼容老版本接口
2、依赖多个外部的类时,使用适配器模式统一多个类的接口设计
3、添加或替换的新依赖可能会影响到现有的逻辑,使用适配器模式将新依赖的接口进行二次封装以减轻对原有逻辑的影响
4、适配不同格式的数据,统一为相同的格式以方便存储和使用
通用实现流程:
1、确定需要适配的类的接口
2、使用继承或组合的方式创建适配器类(对象、方法),统一接口名称和输入输出,并实现接口具体逻辑
3、在业务代码中使用适配器提供的方法
示例
改变电压,国内普通民用电压一般为 220V,但是日本的民用电压为 110V,如果购买了日本的马桶就需要将 220V 转为 110V
1 | // 日本产的马桶 |
桥接模式
桥接模式是指一个类存在两个(或多个)独立变化的维度,通过组合(关联)的方式,让这两个(或多个)维度可以独立进行扩展。通过组合关系来替代继承关系,避免继承层次的指数级爆炸。
通用实现流程:
1、按照维度创建多个类 A、B、C…
2、每个类要提供可以将其他维度实例设置为自身属性的方法
3、将其中一个类 A 作为主体,将其他维度(B、C…)的实例组合到主体类 A 的实例对象上
示例
车有两个维度:
1、品牌有奔驰、宝马、奥迪等 M 种
2、档位类型有手动挡、自动挡、手自一体等 N 种
所有品牌和档位的结合有M*N
种,那么按照继承来实现所有的车就要M*N
类。但如果使用桥接模式,先 new 一个具体的品牌车,再 new 一个具体的档位,然后 车.set(档位) ,这样实现所有的车只需要M+N
种,车和档位的种类和组合都可以自由变化。
1 | // 品牌类 A |
外观模式
外观模式也叫门面模式,通过外观模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。外观模式做接口整合,将内部复杂的具体实现则对开发者隐藏了起来,解决了多接口调用带来的问题。但外观模式也有缺点,它可能会带来一些性能问题,而且不符合开闭原则,一旦业务需求改变就需要修改外观类。
适合的业务场景:
1、通过外观模式封装一些接口用于兼容多浏览器。
2、整合接口,向外部提供更方便的接口。假设有一个系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用 A 系统的 a、b、d 接口。利用外观模式,可以提供一个包裹 a、b、d 接口调用的外观接口 x,给系统 B 直接使用。
3、维护老项目时,通过使用外观模式封装一个新类,让新增业务与新类交互,新类与老业务遗留代码交互。
通用实现流程:
1、创建子系统类,确定需要整合的接口
2、创建外观类,统一接口名称和输入输出,并整合子系统中的接口,实现具体逻辑
3、在业务代码中使用外观类提供的方法
示例
1、兼容浏览器事件绑定
1 | let addEvent = function (el, ev, fn) { |
2、axios 兼容 node 和浏览器两种环境axios 核心逻辑
1 | // axios 获取默认适配器的方法 |
3、整合接口
1 | // 4 个子系统接口 |
享元模式
享元模式的意图是复用对象,节省内存。享元模式要求将对象的属性分为内部状态和外部状态。内部状态独立于具体的场景,通常不会改变,可以被一些对象共享;外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。享元模式的主要目的是节省内存,是一种以时间换空间的设计模式。单例或多例模式缓存对象主要是为了控制全局中实例的个数;代理缓存等缓存数据主要是为了提高访问效率,主要目的是节省时间;对象池等池化技术一般是为了重复使用创建好的对象,减少初始化对象的消耗,主要目的也是节省时间。
通用实现流程:
1、创建享元类,分离对象的内部状态和外部状态
2、创建享元工厂类,实现将对象内部状态缓存到享元池的接口
3、创建非享元类,提供生成最终完整对象的方法
示例
为城市所有的汽车进行登记
1 | // 享元类 |
组合模式
组合模式是指将一组对象组织成树形结构,以表示一种“部分-整体”的层次结构。组合模式使得用户对单个子叶对象和组合对象的使用具有一致性,简单来说就是把对象之间组合成树状结构,能通过一个元素的方法调用它的所有子元素的同名方法。可以看出,组合模式与组合关系没有一点关系,组合模式主要是用来处理树形结构数据,应用场景比较局限,能用到组合模式的肯定是树形结构。
通用实现流程:
1、创建抽象构件类,为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。
2、创建树叶构件类,继承并实现抽象构件类中的方法
3、创建树枝构件类,继承并实现抽象构件类中的方法,特别是与树叶同名的方法
4、将组合对象与子叶对象都组成树形结构
示例
管理文件夹和文件
1 | // 树叶构件类 文件 |