前情回顾
我们在 史上最全的JS设计模式(一) 中主要介绍了设计模式的定义、好处、原则和类别,并详细解释了13种设计模式。
本文将继续介绍剩下的9经典模式,以及设计模式的JS代码示例。:
类型
|
目的
|
模式
|
核心
|
创建型
|
**处理对象创建机制**
使得程序可以更加灵活的判断针对某个给定实例需要创建哪些对象。 |
单例模式
|
确保一个类只有一个实例,并提供对该实例的全局访问。
|
简单工厂模式
|
一个工厂类根据传入的参量决定创建出那一种产品类的实例。
| ||
工厂模式
|
定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。
| ||
抽象工厂模式
|
为一个产品族提供了统一的创建接口。当需要这个产品族的某一系列的时候,可以从抽象工厂中选出相应的系列创建一个具体的工厂类
| ||
建造者模式
|
封装一个复杂对象的构建过程,并可以按步骤构造
| ||
原型模式
|
用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
| ||
结构型
|
**处理对象的组合**
将对象结合在一起形成更大的结构 |
代理模式
|
通过替身对象实现对访问动作的控制和处理
|
适配器模式
|
将一个类的方法接口转换成客户希望的另外一个接口
| ||
装饰器模式
|
动态的给对象添加新的功能
| ||
组合模式
|
将对象组合成树形结构以表示“”部分-整体“”的层次结构
| ||
享元模式
|
通过共享技术来有效的支持大量细粒度的对象
| ||
外观模式
|
对外提供一个统一的方法,来访问子系统中的一群接口
| ||
桥接模式
|
将抽象部分和它的实现部分分离,使它们都可以独立的变化
| ||
行为型
|
**改善或者简化系统中不同对象之间的通信**
|
观察者模式
|
定义了对象一对多的依赖关系。当目标对象状态发生变化后,会通知到所有的依赖对象
|
模版模式
|
抽象父类定义抽象方法和具体运行策略,来制定子类的运行顺序和机制;
具体子类来重写父类的抽象方法
| ||
策略模式
|
定义多个策略类实现具体算法
定义一个环境类通过请求参数来决定使用哪些策略
| ||
状态模式
|
允许一个对象在其对象内部状态改变时改变它的行为
| ||
中介者模式
|
用一个中介对象来封装一系列的对象交互
| ||
迭代器模式
|
提供一种方法顺序访问一个聚合对象中的各个元素,不需要关心对象的内部构造
| ||
备忘录模式
|
在不破坏封装的前提下,保持对象的内部状态
| ||
访问者模式
|
在不改变数据结构的前提下,增加作用于一组对象元素的新功能
| ||
解释器模式
|
给定一个语言,定义它的文法的一种表示,并定义一个解释器
| ||
命令模式
|
将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
| ||
职责链模式
|
将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会
|
【创建型】
建造者模式
建造者模式就解决这种场景的创建型设计模式,它可以将一个复杂的对象分解成多个简单的对象来进行构建,将复杂的构建层与表现层分离,使相同的构建过程可以创建不同的表示模式。
举一个比较通俗的例子,我需要生产一个奔驰车:首先需要定义一个Car的Product类,然后定义一个BenzBuilder建造者和CarDirector指挥者
// 定义Car的Product类
function Car(){
this.color='' // 颜色
this.brand='' // 品牌
// ...其他复杂属性
}
// 定义BenzBuilder建造者
function BenzBuilder(){
this.car = new Car()
this.genColor =()=>{
this.car.color = 'black'
}
this.genBrand = ()=>{
this.car.brand = 'benz'
}
}
// 定义CarDirector指挥者
function CarDirector(){
this.genCar = (builder)=>{
// 分步调用builder的函数,生成Car对象的属性
builder.genColor()
builder.genBrand()
return builder.car
}
}
var benzBuilder = new BenzBuilder()
var director = new CarDirector()
// 调用
var car = director.genCar(benzBuilder) // Car {color: "black", brand: "benz"}
// 定义BenzBuilder建造者
function BaomaBuilder(){
this.car = new Car()
this.genColor =()=>{
this.car.color = 'red'
}
this.genBrand = ()=>{
this.car.brand = 'baoma'
}
}
var baomaBuilder = new BaomaBuilder()
// 调用
var car2 = director.genCar(baomaBuilder) //Car {color: "red", brand: "baoma"}
综上我们可以看到,建造者模式的适用条件有:
-
对象(Product)的属性/结构复杂
-
对象的属性生成是有一定逻辑和变化的(不同的Builder有不同的属性生成函数)
-
不同实例对象的属性组装顺序是不变的(Director指挥顺序不变)
优点
- 可拓展性高,碰到新的需求,只需要实现一个新的建造者
缺点
-
对Product对象有要求,需要抽取满足各类情况的复杂对象
-
指挥者的调用顺序不能变,构建过程必须相同
区别:
-
与工厂模式的区别:建造者关注过程,由指挥者调用构建过程;工厂模式不关注过程
-
与外观模式、模版模式的区别:建造者是创建复杂结构的对象(创建型),外观模式是整合一群接口对外提供统一方法(结构型),模版模式是按照模版执行动作(行为型)
推荐程度:⭐️⭐️⭐️⭐️
【结构型】
享元模式
享元模式对对象的重用提供了一种解决方案,它使用共享技术对相同或者相似对象实现多次复用,使得系统中的对象个数大大减少。同时享元模式使用了内部状态和外部状态,同时外部状态相对独立,不会影响到内部状态,所以享元模式能够使得享元对象在不同的环境下被共享。
// 健康测量
function Fitness(name, sex, age, height, weight) {
this.name = name;
this.sex = sex;
this.age = age;
this.height = height;
this.weight = weight;
}
// 开始评判
Fitness.prototype.judge = function() {
var ret = this.name + ': ';
ret += this.sex === 'male'?this.judgeMale(): this.judgeFemale()
console.log(ret);
};
// 男性评判规则
Fitness.prototype.judgeMale = function() {
var ratio = this.height / this.weight;
return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};
// 女性评判规则
Fitness.prototype.judgeFemale = function() {
var ratio = this.height / this.weight;
return this.age > 20 ? (ratio > 4) : (ratio > 3);
};
var a = new Fitness('A', 'male', 18, 160, 80);
var b = new Fitness('B', 'male', 21, 180, 70);
var c = new Fitness('C', 'female', 28, 160, 80);
var d = new Fitness('D', 'male', 18, 170, 60);
var e = new Fitness('E', 'female', 18, 160, 40);
// 开始评判
a.judge(); // A: false
b.judge(); // B: false
c.judge(); // C: false
d.judge(); // D: true
e.judge(); // E: true
因为性别是主要影响评判方法的属性,所以可以把性别看做内部状态即可,其他属性都属于外部状态。将对象的公共部分(内部状态)抽离出来,与外部状态独立。
// 健康测量
function Fitness(sex) {
this.sex = sex;
}
// 工厂,创建可共享的对象
var FitnessFactory = {
objs: [],
create: function(sex) {
if (!this.objs[sex]) {
this.objs[sex] = new Fitness(sex);
}
return this.objs[sex];
}
};
// 管理器,管理非共享的部分
var FitnessManager = {
fitnessData: {},
// 添加一项
add: function(name, sex, age, height, weight) {
var fitness = FitnessFactory.create(sex);
// 存储变化的数据
this.fitnessData[name] = {
age: age,
height: height,
weight: weight
};
return fitness;
},
// 从存储的数据中获取,更新至当前正在使用的对象
updateFitnessData: function(name, obj) {
var fitnessData = this.fitnessData[name];
for (var item in fitnessData) {
if (fitnessData.hasOwnProperty(item)) {
obj[item] = fitnessData[item];
}
}
}
};
// 开始评判
Fitness.prototype.judge = function(name) {
// 操作前先更新当前状态(从外部状态管理器中获取)
FitnessManager.updateFitnessData(name, this);
var ret = name + ': ';
ret += this.sex === 'male'?this.judgeMale(): this.judgeFemale()
console.log(ret);
};
// 男性评判规则
Fitness.prototype.judgeMale = function() {
var ratio = this.height / this.weight;
return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};
// 女性评判规则
Fitness.prototype.judgeFemale = function() {
var ratio = this.height / this.weight;
return this.age > 20 ? (ratio > 4) : (ratio > 3);
};
var a = FitnessManager.add('A', 'male', 18, 160, 80);
var b = FitnessManager.add('B', 'male', 21, 180, 70);
var c = FitnessManager.add('C', 'female', 28, 160, 80);
var d = FitnessManager.add('D', 'male', 18, 170, 60);
var e = FitnessManager.add('E', 'female', 18, 160, 40);
// 开始评判
a.judge('A'); // A: false
b.judge('B'); // B: false
c.judge('C'); // C: false
d.judge('D'); // D: true
e.judge('E'); // E: true
应用场景:
- 存在大量重复的对象需要处理
优点:
- 节省内存,避免大量创建重复对象
缺点:
-
享元模式要求能够共享的对象必须是细粒度对象
-
享元模式会使得系统变得更加复杂
推荐程度:⭐️⭐️
桥接模式
所谓桥接,指的是将抽象和实现部分进行分离和连接的方式。桥接模式可以将抽象部分与它的实现部分分离,使它们都可以独立地变化。桥接模式将继承关系转化成关联关系,封装了变化,完成了解耦,减少了系统中类的数量,也减少了代码量。
比如说小阮是一个高大帅气的男孩,小迟是一个高大聪明的男孩。我们可以将高大、帅气、聪明抽取成单独的特性类,由不同的特性类去完成具体同学的组装。
// 定义特性类
class Tall {
constructor(height){
this.height = height
}
descript(){ console.log('您可真高') }
}
class Handsome {
descript(){ console.log('帅气!') }
}
class Smart {
descript(){ console.log('别人家的孩子真聪明') }
}
// 定义具体实现类
// 小阮是一个高大帅气的男孩
class XiaoRuan {
constructor(height){
this.height = new Tall(height)
this.face = new Handsome()
}
init(){
this.height.descript()
this. face.descript()
}
}
// 小迟是一个高大聪明的男孩
class XiaoChi {
constructor(height){
this.height = new Tall(height)
this.head = new Smart()
}
init(){
this.height.descript()
this.head.descript()
}
}
应用场景:
- 系统存在多个维度的变化,需要将这些维度抽离出来,让其独立变化
优点:
-
遵行单一职责、开闭原则,系统拓展性好
-
抽象和实现充分解耦
缺点:
- 会有大量的抽象类,增加开发成本
区别:
-
与建造者模式都是构建复杂对象/结构,比较容易混淆。但是建造者是创建型模式,需要先定义稳定的Product类,后续都是为了构建Product实例,如果需要增加Product属性,需要同步修改所有的Builder的方法、Director的调用过程。而桥接模式是组合型模式,通过不同的类组合成复杂结构,如果需要增加实例的属性,只需要增加额外的抽象类即可。
-
与装饰者模式的区别是,装饰者是围绕一个已有的实例类来进行装饰增强,而桥接模式是通过多个抽象类来组装一个对象。
推荐程度:⭐️⭐️⭐️
【行为型】
迭代器模式
eg:
Array.forEach Object.keys Map.forEach Set.forEach
如【阮一峰ES6 Iterator/for of】中描述,迭代器主要的作用是:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;
function makeIterator(array,cb) {
var nextIndex = 0;
return {
next: function() {
var result= nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
cb(result);
return result
},
init:function(){
var result ={}
while(result.done!==true){ result=this.next() }
}
};
}
var it = makeIterator(['a', 'b'], console.log);
it.init()
// { value: "a", done: false }
// { value: "b", done: false }
// { value: undefined, done: true }
应用场景:
- 需要遍历访问对象里的所有元素进行操作
优点:
- 可以顺序遍历元素,而不需要关心对象内部元素的具体形式
缺点:
- -
推荐程度:⭐️⭐️⭐️⭐️
备忘录模式
备忘录模式,顾名思义像一个小本本,记录着我们的安排和想法,方便随时翻阅和回忆。在软件开发中,备忘录模式主要是在不破坏分装性的前提下,去捕获和记录对象的状态,提供对象状态恢复的能力,常用于记录缓存、记录历史、撤销操作
- 前端权限存储:我们请求单个页面的权限数据后,通常会缓存到浏览器中,本次会话中如果再次访问了这个页面,可以取出缓存的权限数据进行处理。
- 撤销操作:在流程设计、应用中心页面设计中,通常会存储用户历史操作,通过读取history来撤销对schema的修改
适用场景:
- 保存一个对象在某一时刻的全部或部分状态
优点:
-
提供了一种状态恢复的时间机制,使得用户可以方便的回退到一个特定的历史步骤。
-
备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。
缺点:
- 备忘录模式的主要缺点是资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免的需要占用大量的存储空间。
推荐程度:⭐️⭐️⭐️
访问者模式
人员结构定义如下
class Employee {
constructor(name, remainingHolidays, dgree, performance){
this.name=name //名称
this.remainingHolidays= remainingHolidays // 剩余假期天数
this.dgree=dgree // 雇员级别 1初级,2中级,3高级
this.performance=performance // 绩效 1不及格 2及格 3优秀
}
}
人员组结构定义如下
class EmployeeGroup {
this.employeeMap=new Map()
// 新增员工
addEmployee(employee){
this.employeeMap.set(employee.name, employee)
}
}
窝漕,这不是很简单吗,EmployeeGroup增加一个遍历循环不就搞定了吗?
class EmployeeGroup {
this.employeeMap=new Map()
addEmployee(employee){
this.employeeMap.set(employee.name, employee)
}
// 遍历输出结果
countMoney(){
for(let employee of this.employeeMap.values()){
const {name,remainingHolidays, dgree, performance} = employee
const result= 200* remainingHolidays * dgree * performance
console.log(name, result )
}
}
}
访问者模式就可以用于解决这个问题。
首先我们对Employee对象和EmployeeGroup对象进行增强,增加accept方法器接受访问者Visitor
class Employee {
constructor(name, remainingHolidays, dgree, performance){
this.name=name //名称
this.remainingHolidays= remainingHolidays // 剩余假期天数
this.dgree=dgree // 雇员级别 1初级,2中级,3高级
this.performance=performance // 绩效 1不及格 2及格 3优秀
}
accept(visitor){
visitor.visit(this)
}
}
class EmployeeGroup {
employeeMap=new Map()
addEmployee(employee){
this.employeeMap.set(employee.name, employee)
}
// 遍历accept
accept(visitor){
for(let employee of this.employeeMap.values()){
employee.accept(visitor)
}
}
}
// 抽象访问者,定义访问的接口
class MoneyVisitor{
visit(employee){
throw new Error('need overwrite this function')
}
}
// 2018年的福利规则
class MoneyVisitorIn2018 extends MoneyVisitor{
visit(employee){
const {name,remainingHolidays, dgree, performance} = employee
const result= 200* remainingHolidays * dgree * performance
console.log(name, result )
}
}
const group = new EmployeeGroup()
group.addEmployee(new Employee('小王',3,1,1))
group.addEmployee(new Employee('小李',1,2,3))
group.accept(new MoneyVisitorIn2018())
// 小王 600
// 小李 1200
**By the way。**我们常见代码中有哪些访问者模式了?
- Js 的call和apply就是访问者模式的精髓,我们可以不去改变执行对象this的结构的同时,为它添加新的操作方法,来实现对操作对象的访问!
- vue的Dep和watch的绑定也是访问者模式。dep可以看作是被访问对象,watcher可以看作成访问对象:
- 在defineReactive中通过dep.depend()建立观察
- depend方法是找到当前的watcher,然后调用watcher.addDep(this),【hint:可以看成是accept方法】
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
- 然后在wather的addDep方法中是具体对dep对象的访问操作
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
适用场景:
- 对象数据结构稳定,但是操作的方法易变
优点:
-
符合单一职责原则
-
优秀的扩展性,操作方法可以灵活变化
缺点:
-
具体元素对访问者公布细节,违反了迪米特原则
-
具体元素变更比较困难。
-
违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
区别:
- 访问者模式关注对对象的操作,迭代器模式关注对对象的迭代
推荐程度:⭐️⭐️⭐️
解释器模式
**解释器模式:**对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。
{
label:'名称',
field:'name',
component:'el-input',
props:{
clearable:true
},
events:{
change:()=>{}
}
}
<el-form-item path={item.field}>
<template slot="label">
{item.label}
</template>
<renderCompStr
{...item.props}
on={item.events}
>
</renderCompStr>
</el-form-item>
适用场景:
-
一些重复出现的问题可以用一种简单的语言来进行表达
-
语言可以进行抽象和组合
-
简单语法需要解释的场景
优点:
-
语言可以按照特定语法进行组合,相对于策略模式,可以处理的场景更加多、更复杂
-
可扩展性比较好,可以通过增加了新的解释表达式的方式来满足新的操作
缺点:
- 复杂语言比较难维护
推荐程度:⭐️⭐️⭐️
命令模式
命令模式的定义是:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
const obj={
say:()=>{
console.log('hi')
},
eat:()=>{
console.log('eating...')
}
}
// 命令模式调用obj的say方法
obj.say()// 命令模式调用obj的say方法
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}})
store.dispatch('increment',1)
适用场景:
-
行为的请求者与行为的处理者耦合度过高,需要进行解耦
-
调用方只关心命令的发送,不关心处理的过程
优点:
-
调用方和执行方高度解耦
-
满足开闭原则,可以非常方便的增加新的命令
缺点:
- 可能会存在过多的命令
推荐程度:⭐️⭐️⭐️⭐️
职责链模式
职责链模式的定义:使多个对象都有机会处理请求,从而避免了请求的发送者与多个接收者直接的耦合关系,将这些接收者连接成一条链,顺着这条链传递该请求,直到找到能处理该请求的对象。
使用场景:
-
有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
-
在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
-
可动态指定一组对象处理请求。
优点:
-
降低耦合度。它将请求的发送者和接收者解耦。
-
简化了对象。使得对象不需要知道链的结构。
-
增加新的请求处理类很方便。
缺点:
-
不能保证请求一定被接收。
-
系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
-
可能不容易观察运行时的特征,有碍于除错。
推荐程度:⭐️⭐️⭐️
思考题
现在low code和 no code方案逐渐热了起来,大家或多或少使用/看过相关的产品,比如说amis、anole-form、formly等。这些产品都各有一些缺点和局限性。
那如果需要你来设计一套low code方案,结合你现在的需求,你会实现哪些功能模块、应用哪些设计模式?面对自己认为麻烦的特性和功能,如何进行设计优化?
有想法的同学可以在下方留言自己的设计思路,我也会结合自己在low code上的经验和大家进行讨论。