设计模式是我在2018年在成都工作的时候听 肉山老师 的直播JavaScript 设计模式 导言的时候就想学习的一个东西,但是一直拖拖拖,虽然后来买了肉老师的 Chat 从 jQuery 里学习设计模式,但是看了一头雾水,后来同样买了肉老师推荐的书《设计模式:可复用面向对象软件的基础》,也是在家里吃灰了半年多。
一直想把整个设计模式都了解一下,不用吃透大概明了就可以,但是每次都提不起开坑的念头,如果再写成学习笔记的话,学习周期会大大拉长。
这次在项目的不断迭代中,我遇到了一个问题,就是 项目中的类型和状态管理,所以就想着是否有一个合适的设计模式来解决问题,所以就被逼迫着捡起来了……
肉老师谈论如何学习设计模式的方法,和我的学习想法是类似的,不管是学习设计模式还是学习一个框架/库,先了解一个大概心里有一个底,然后在实际遇到问题的时候再去找文档来炒冷饭,我觉得这样会更清晰一些,但是他不推荐了解的那么多设计模式,只需要过一遍,等遇到了差不多的场景再去具体看适合哪一个。
《设计模式:可复用面向对象软件的基础》这本书是 1995 出版的,可能会有一些问题,或者有一些新的设计模式出现,但都不是问题,咱先把这本书内的 23
个设计模式了解,之后再看情况去了解新的内容。
所以这篇文章应该会陆陆续续的更新一些新内容,好了,在这篇标号 00 的笔记中我就以前端开发者的视角按照设计模式名称+概括+简单示例,来大概描述一下每种设计模式,如果有遇到可用场景时再深入的去记录。
这本书内把设计模式分成 三种类型
( 创建型、结构形、行为型) 和 二十三个模式
创建型
1. 工厂方法 Factory Method
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂模式使一个类的实例化延迟到其子类。
工厂模式在实际应用中有很多,比如前端er最了解的 jQuery 中的 $()
方法查看文档,传入一个类名或者ID或者其它的值,返回回来一个DOM实例。中间的一些过程就不需要关注了,直接使用 $()
方法就会必定返回一个DOM实例。
讲人话,就是传入一些值,返回一个符合预期的结果。
2. 抽象工厂 Abstract Factory
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂模式是一个比较复杂的创建型模式,如果用最简单的话来说,可以描述成基于一组工厂方法,传入数据之后返回一个复杂对象,中间的步骤不需要关注。
有点类似于Vue中的高阶组件中包含了很多子组件和一些其它元素最后展现出一个渲染结果。
用廖雪峰老师的例子来举例就很容易让人明白 我们希望为用户提供一个Markdown文本转换为HTML和Word的服务 - 抽象工厂,虽然示例是Java,但是不影响理解。
3. 生成器 Builder
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
生成器模式,有些地方也叫建造者模式,用简单的话来概括就是传入一个或者一组数据,最后拼接返回一个结果,用Vue来举例的话就有点像 Reander
函数。
乍一看可能和抽象工厂很像,对确实很像,他们都是基于工厂模式的扩展,主要的区别就是抽象工厂模式对于中间的数据没有要求,而生成器重点关注如何分步生成复杂对象。
争议
这边的具体区别我还没有彻底弄明白确实需要整理之后来区别一下。具体得等这篇文章最后了。
4. 原型模式 Prototype
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
这个在Javascript中并不需要多理解,因为Js中每个数据类型都是对象,都拥有一个 prototype
属性,当然你也可以自己用代码来实现自己得原型模式其实显得很奇怪。大概可以用构造函数的视角来理解,只不过构造函数生成的实例对象的属性都是独立的,所以引入了 prototype
属性,来做到数据共享(修改其中一个,影响到其它所有)。
同样JS有提供一个原生的实现 Object.create()
5. 单例模式 Singleton
保证一个类仅有一个实例,并提供一个访问它的全局访问点
类似于 window
对象,全局确保只有一个我们可以使用的 window
实例。
具体业务使用场景中,Vuex
也是单例模式,全局只会有一个Store实例来供用户去读取和修改。
我在查阅资料的时候稍微看到了还有人提到 惰性单例模式
,这个之后再去了解
结构形
1. 适配器 Adapter
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适配器模式的话,我们可以这样来理解,很多时候我们不确定客户端浏览器是否支持我们所使用的API,这个时候就会写兼容,大概来写一个伪代码段来举例
var getByClass = function (className) {
if(document.querySelector) {
return document.querySelector(className);
} else {
return document.getElementsByClassName(className)
}
};
就是那么简单,虽然有点强行解释,但这个就是适配器….
2. 桥接 Bridge
将抽象部分与它的实现部分分离,使它们都可以独立地变化
这个我本来想用 UI 库中的 Notification
组件来举例的,因为只需要调用 notification[type].open({config})
函数传入几个参数就可以展现出不同的消息通知栏了,
但是隔了一天之后发现拿这个例子来讲好像不是很的能解释抽象
和实现
的分离,所以我用系统中的画图附件来举例会好理解很多。
在画版中会有很多不同的 工具
,比如说画笔,图形和文字等,同样他们都有颜色、大小这些个 属性
,
我们在使用的时候只需要选择 工具
,然后选择 属性
就可以绘制出我们所需要的结果了。
我们可以 单独 的去 变化组合 工具、属性,并不会出现红色画笔和黑色画笔这种 带有属性的工具(没有强耦合),
如果后期扩展了新得工具只要单独增加新工具的业务逻辑就行,不需要去关注它的大小和颜色这些属性,同样增加新属性的时候也同理。
3. 组合 Composite
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
组合模式简单来说具有统一接口的一组树形结构。父级和子级拥有相同的接口,比如系统的文件管理,
每个文件夹都可以包含具体文件和文件夹,并且每个文件/文件夹都有类似的功能,比如说创建/删除/重命名。
在执行删除功能的时候开始递归执行子级的删除操作,每个子级都被删除后执行父级的删除操作。
4. 装饰 Decorator
动态地给一个对象添加一些额外的职责。就增加功能来说,相比生成子类更为灵活。
一般来说我们都会直接想到使用类的继承,然后在子类上附加新的功能,但是继承为类引入静态特征,并且随着扩展功能的增多,子类就会爆炸式增长。所以我们不想增加很多子类的情况下扩展类,就会用到装饰器模式。
看到一个很形象的比喻,现在UI库中的表单组件的表单验证,在不同的表单组件上使用不同的校验规则,而且并不影响组件本身,只是在提交之前额外调用了传入的校验函数。
5. 外观 Facade
为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
简单概括就是,在日常工作中会编辑出来很多子功能,这些子功能只有你清楚,如果你想给同事使用,就可以l考虑外观模式,整合起来并且开放一个相对统一的调用接口。
6. 享元 Flyweight
运用共享技术有效地支持大量细粒度的对象。
享元模式的话,我描述一个场景来帮助理解,比如说现在有一个气球里边装了几千个小球,这些小球材质大小一致,并且有三种颜色,如果我们为每一个气球都船舰一个对象,就会浪费大量的内存,这个时候我们就可以把相同的材质和大小和颜色提取出来,通过外部引用或者继承的方式来共享这些属性。
7. 代理 Proxy
为其他对象提供一种代理以控制对这个对象的访问。
这个的话,学习过Vue的前端就很清晰了,Vue对一些数据的操作进行了代理,去检查是否需要更新视图。
ES6中也新增了 Proxy 这个API,并且Vue3中使用 Proxy
代替了 defineProperty
。
行为型
1. 职责链 Chain of Responsibility
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
简短的来说就是分工合作,比如说工厂的流水线,每一个工作站都有自己的工作,完成自己的工作(加工/质检)之后通过传输带到下一个工作站进行处理,最后出厂一个成品的手机。
2. 命令 Command
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
设计思想是把命令拆分开来,创建和执行分离,使得调用者无需关心具体的执行过程,大概是,每天中午点餐的时候只要和老板娘喊一嗓子炒年糕,一会老板就会端出来一盆炒年糕。
3. 解释器 Interpreter
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
快速的话可以理解成 正则,把一系列复杂的规则判断用简单的语句实现。
4. 迭代器 Iterator
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。可以用对象的 Object.entries()
方法来理解。每一个对象都有它自己的迭代方法,你不需要知道它具体有哪些属性,它可以按照顺序去输出对象的内部元素。
5. 中介者 Mediator
用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
MVC
框架中的 C
(控制器),它就是用来控制 M(模型)和 V(视图)的中介者。
6. 备忘录 Memento
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
一般来描述这个模式都是举例文本编辑器的撤销功能,并且可以一直退回到空白文本的状态,其它的类似还有,浏览器的后退前进,数据库的事务回滚等等。
保持对象的状态,并且可以在调用组件生命周期内持久保存这些快照。
7. 观察者 Observer
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
这个直接用Vue来举例吧,data
内 声明 的变量被修改时,会自动通知UI更新。
最直接可以看到的就是每一个在data
内声明的变量都会有一个 __ob__
对象。
观察者模式和中介者模式其实非常的相似,只不过这里的 “中介者/订阅者” 就是观察者
8. 状态 State
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
可以描述一个应用场景,在一个后台管理系统中,一定会有审核相关的部分,每一条不同状态的记录都会对应不同的操作,这边就会适合状态模式的应用。
9. 策略 Strategy
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
也描述一个应用场景吧,比如你现在老板给你一个任务,让你做一个根据职位、绩效、考勤和工资自动计算奖金的程序,不同的职务有不同的标准,不同的绩效等级也会影响奖金系数。
一开始你可以使用 if...else
,去判断不同的场景,随着需求变化,代码不断调整,越来越臃肿。代码几乎不能维护,因为你根本不知道这里的一处改动会对后面造成什么样的影响。
这时候可以使用策略模式来更具不同的条件执行不同的业务逻辑。
10. 模板方法 Template Method
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
这个很容易解释,我们所熟悉的 Ajax
函数就是,基本流程都确定,只需要接受 URL 地址 和 Settings 对象 就可以,再把需要执行的操作放在回调函数里。
它封装了不变部分,扩展可变部分。在父类中提取了公共的部分代码,便于代码复用,而把可变部分算法由子类继承实现,便于子类继续扩展。
11. 访问者 Visitor
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
核心思想是为了访问比较复杂的数据结构,不去改变数据结构,而是把对数据的操作抽象出来,在“访问”的过程中以回调形式在访问者中处理操作逻辑。
这个还没有想到合适的场景,大概就是说 有一个稳定的数据结构,但是需要实现的功能并不确定,针对不同的访问者有不同的行为职责。
感觉有点类似命令模式,命令模式Plus?
不断了解各种设计模式的的时候,发现其实不同的设计模式之间可能并没有很明确的界限,并且这些传统的设计模式在前端工作中很多都使用不上,应该可以简略成几个模式即可,不需要每一个都了解,反而可能影响自己的认知。
附
《设计模式:可复用面向对象软件的基础》
《深入设计模式》一本关于设计模式及其背后原则的电子书籍
JavaScript设计模式与开发实践 - 曾探
设计模式 | 菜鸟教程
设计模式 - 廖雪峰
从 jQuery 里学习设计模式 - Meathill
Javascript继承机制的设计思想 - 阮一峰
JavaScript中的设计模式