# 👉 设计模式
设计模式定义:在软件设计中针对不同场景问题的解决方案,使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。
设计模式的核心思想,就是“封装变化”。何为“封装变化”:
这一点相信大家不难理解——如果说我们写一个业务,这个业务是一潭死水,初始版本是 1.0,100 年后还是 1.0,不接受任何迭代和优化,那么这个业务几乎可以随便写。反正只要实现功能就行了,完全不需要考虑可维护性、可扩展性。但在实际开发中,不发生变化的代码可以说是不存在的。
我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定。这个过程,就叫“封装变化”;这样的代码,就是我们所谓的“健壮”的代码,它可以经得起变化的考验。而设计模式出现的意义,就是帮我们写出这样的代码。
设计模式主要有 23 种,分为三大类型为:创建型模式、结构型模式、行为型模式。
这里就不一一详细展开,主要用作简要记录常用的设计模式。
# 创建型模式
关注对象创建机制,以增加灵活性和复用性。
# 工厂模式
- 目的:提供一个统一对外用于创建对象的接口,而无需指定具体的类。将不同的 new 操作封装起来,根据不同的参数创建类型相似但细节不同的对象。
说到工厂模式之前,看看构造器是什么:
// 场景1:一个公司需要录入很多员工信息
function User(name, age, career) {
this.name = name;
this.age = age;
this.career = career;
}
const user1 = new User(name, age, career);
User 就是一个类的构造器(构造函数),构造器将 name、age、career 赋值给实例对象的过程封装,确保了每个对象都具备这些属性,确保了共性的不变,同时将 name、age、career 各自的取值操作开放,确保了个性的灵活。
构造器模式的作用就是去抽象每个实例对象的变和不变;而工厂模式也有点类似意思,不过主要是抽象不同构造函数的变和不变。
那什么是工厂模式?我们往下看看应用场景
// 场景2:在场景1的基础上,需要对员工分类及其主要职责进一步分类赋值,程序员主要工作是敲代码,产品经历主要工作是催更,老板主要工作见客户
// 提炼不同类的共性
function User(name, age, career, work) {
this.name = name;
this.age = age;
this.career = career;
this.work = work;
}
// 对不同类的个性进行划分
function UserFactory(name, age, career) {
let work
switch(career) {
case 'coder':
work = ['写代码', '修Bug']
break
case 'product manager':
work = ['写PRD', '催更']
break
case 'boss':
work = ['看报', '见客户']
case 'xxx':
// 其它工种的职责分配
...
}
return new User(name, age, career, work)
}
const coder1 = userFactory(name, age, 'coder');
const boss1 = userFactory(name, age, 'boss');
工厂模式其实就是独立出一个大类,将创建实例对象的过程用大类的子类来实现。即将 new 操作单独封装,只对外提供接口,参数需要怎么区分来我可以不关心。
工厂模式的核心作用如下:
- 主要用于隐藏创建实例的复杂度,只需对外提供一个接口;
- 实现构造函数和创建者的分离,满足开放封闭的原则(对扩展开放和对修改封闭)。
工厂模式主要有三种细分模式:简单工厂模式、工厂方法模式、抽象工厂模式。
# 应用场景
平时遇到要创建实例的时候,就可以想想能否用工厂模式实现了。在写了大量构造函数、调用了大量的 new、自觉非常不爽的情况下,我们也应该思考是不是可以掏出工厂模式重构我们的代码了。比如:手机工厂(抽象出操作系统、硬件芯片、屏幕...)
# 单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。
“Vuex 全局状态管理器的 Store 是一个典型的单例模式应用,确保全局只有一个状态树。”
实现的方法:先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
// 例子:全局模态弹窗
class Modal {
show() {
console.log("打开弹窗");
}
hide() {
console.log("关闭弹窗");
}
static createModal() {
// 判断是否已经new过实例
if (!Modal.instance) {
// 若这个唯一的实例不存在,那么先创建它
Modal.instance = new Modal();
}
//若已经存在,则直接返回
return Modal.instance;
}
}
const modal1 = Modal.createModal();
const modal2 = Modal.createModal();
modal1 === modal2; // true
// createModal 的闭包实现
Modal.createModal = (function() {
let instance = null;
return function() {
if (!instance) {
instance = new Modal();
}
return instance;
};
})();
# 应用场景
在日常业务场景中,我们经常会遇到需要单例模式的场景,比如最基本的弹窗,或是购物车等。因为不论是在单页面还是多页面应用程序中,我们都需要这些业务场景只会同时存在一个。
生产实践例子:vuex 状态管理中的 store
来自其他大佬博客的一篇参考文章 (opens new window):
// 安装vuex插件
Vue.use(Vuex);
// 将store注入到Vue实例中
new Vue({
el: "#app",
store,
});
// 通过调用 Vue.use()方法安装了 Vuex 插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到 Vue 实例里去。
// 在 install 方法里,有一段逻辑和我们楼上的 getInstance 非常相似的逻辑:
let Vue; // 这个Vue的作用和上述例子中的instance作用一样
export class Store {
constructor(options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== "undefined" && window.Vue) {
install(window.Vue);
}
}
....
}
export function install(_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
"[vuex] already installed. Vue.use(Vuex) should be called only once."
);
}
return;
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue;
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue);
}
# 结构型模式
关注对象和类的组合,以形成更大的结构。
# 装饰器模式
装饰器模式,又名装饰者模式。顾名思义,它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。
目的:动态地给一个对象添加一些额外的职责/新功能,而不改变其本身的结构和功能。
场景:扩展功能(如日志记录、权限检查)、高阶组件(HOC)在 React 中的应用、ES Next 和 TypeScript 的 @decorator 语法。
function moreLog (fn) {
return (...args) => {
console.log('Time:', Date.now())
return fn(...args)
}
}
const detailLog = moreLog(console.log)
detailLog('Hello')
该例中,我们写了一个 moreLog 装饰器,拓展了 console.log
功能。如果后续需要对 console.log
这个行为添加更多详细对需求,可以直接在 moreLog 中添加。如此一来,我们就可以实现“只添加,不修改”的装饰器模式。
ES7 中也引入了装饰器语法插件,具体用法:
npm install babel-plugin-transform-decorators-legacy
npm install babel-cli -g
//.babelrc
// 浏览器和 Node 目前都不支持装饰器语法,需要大家安装 Babel 进行转码:
{
"presets": ["es2015", "latest"],
"plugins": ["transform-decorators-legacy"]
}
// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
target.hasDecorator = true;
return target;
}
// 将装饰器“安装”到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}
// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator)
// 以上简化:
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
# 应用场景
# mixin
function mixins(...list) {
return function(target) {
Object.assign(target.prototype, ...list);
};
}
const Foo = {
foo() {
console.log("in foo");
},
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo(); // in foo
# 属性装饰器
// 装饰器
// 设置属性只读
function readonly(target, name, descriptor) {
// descriptor 属性描述对象(Object.defineProperty 中会用到)
/*
{
value: specifiedFunction,
enumerable: false,
configurable: true
writable: true 是否可改
}
*/
descriptor.writable = false;
}
class Person {
constructor() {
this.first = "周";
this.last = "杰伦";
}
@readonly
name() {
return `${this.first}${this.last}`;
}
}
const p = new Person();
console.log(p.name()); // 打印成功 ,‘周杰伦’
// 试图修改name
p.name = function() {
return "ccc";
};
// Uncaught TypeError:Cannot assign to read only property 'name' of object '#<Person>'
// 可见,再给属性添加了只读的装饰后,代码试图修改属性的命令将会报错。
# 方法装饰器
function log(target, name, descriptor) {
const oldValue = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name} with`, ...args);
return oldValue.apply(this, args);
};
return descriptor;
}
class Math {
@log
add(a, b) {
return a + b;
}
}
const math = new Math();
const result = math.add(1, 2); // 输出: Calling add with 1 2
console.log(result); // 输出: 3
# 适配器模式
目的:将一个类的接口转换成客户希望的另外一个接口,解决兼容性问题。
场景:封装旧库、整合第三方库的 API 使其符合现有代码规范、处理数据格式转换。
适配器模式(Adapter)是将一个 类(对象)的接口(方法或属性) 转化成 适应当前场景的另一个接口,适配器模式使得原本由于接口不兼容的类/对象可以一些工作。所以,适配器模式必须包含目标(Target)、源(Adaptee)和适配器(Adapter)三个角色。
举个例子:
对外提供最新的 Adapter,对旧方法进行一层封装
class OldApi { oldRequest() { return 'old data' } } class Adapter { constructor(oldApi) { this.oldApi = oldApi } request() { return this.oldApi.oldRequest() } } const adapter = new Adapter(new OldApi()) console.log(adapter.request())
对项目中的 ajax 采用 fetch 替换(即适配旧请求格式)
export default class HttpUtils { // get方法 static get(url) { return new Promise((resolve, reject) => { // 调用fetch fetch(url) .then((response) => response.json()) .then((result) => { resolve(result); }) .catch((error) => { reject(error); }); }); } // post方法,data以object形式传入 static post(url, data) { return new Promise((resolve, reject) => { // 调用fetch fetch(url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", }, // 将object类型的数据格式化为合法的body参数 // 转换过程: {a: 1, b: 2} -> a=1&b=2 -> a%3D1%26b%3D2 body: this.changeData(data), }) .then((response) => response.json()) .then((result) => { resolve(result); }) .catch((error) => { reject(error); }); }); } } // Ajax适配器函数,入参与旧接口保持一致 async function AjaxAdapter(type, url, data, success, failed) { const type = type.toUpperCase(); let result; try { // 实际的请求全部由新接口发起 if (type === "GET") { result = (await HttpUtils.get(url)) || {}; } else if (type === "POST") { result = (await HttpUtils.post(url, data)) || {}; } // 假设请求成功对应的状态码是1 result.statusCode === 1 && success ? success(result) : failed(result.statusCode); } catch (error) { // 捕捉网络错误 if (failed) { failed(error.statusCode); } } } // 用适配器适配旧的Ajax方法 async function Ajax(type, url, data, success, failed) { await AjaxAdapter(type, url, data, success, failed); }
# 应用场景
生产实践:axios 中的适配器
// axios常用方式
axios.get('/user?ID=12345')
.then(function (response) {
...
})
.catch(function (error) {
...
})
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
...
})
.catch(function (error) {
...
});
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
axios 强大的地方在于,它不仅仅是一个局限于浏览器端的库。在 Node 环境下,我们尝试调用上面的 api,会发现它照样好使 —— axios 完美地抹平了两种环境下 api 的调用差异,靠的正是对适配器模式的灵活运用。
在 axios 的核心逻辑中,我们可以注意到实际上派发请求的是 dispatchRequest 方法。该方法内部其实主要做了两件事:
1)数据转换,转换请求体/响应体,可以理解为数据层面的适配;
2)调用适配器。
调用适配器的逻辑如下:
// 若用户未手动配置适配器,则使用默认的适配器
// 手动配置适配器允许我们自定义处理请求,主要目的是为了使测试更轻松。
var adapter = config.adapter || defaults.adapter;
// dispatchRequest方法的末尾调用的是适配器方法
return adapter(config).then(
// 请求成功的回调
function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// 转换响应体
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
},
// 请求失败的回调
function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// 转换响应体
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
}
);
// 实际开发中,我们使用默认适配器的频率更高。默认适配器在axios/lib/default.js里是通过getDefaultAdapter方法来获取的:
function getDefaultAdapter() {
var adapter;
// 判断当前是否是node环境
if (
typeof process !== "undefined" &&
Object.prototype.toString.call(process) === "[object process]"
) {
// 如果是node环境,调用node专属的http适配器
adapter = require("./adapters/http");
} else if (typeof XMLHttpRequest !== "undefined") {
// 如果是浏览器环境,调用基于xhr的适配器
adapter = require("./adapters/xhr");
}
return adapter;
}
一个好的适配器的自我修养——把变化留给自己,把统一留给用户。在此处,所有关于 http 模块、关于 xhr 的实现细节,全部被 Adapter 封装进了自己复杂的底层逻辑里,暴露给用户的都是十分简单的统一的东西——统一的接口,统一的入参,统一的出参,统一的规则。
# 代理模式
目的:为其他对象提供一个代理以控制对这个对象的访问。
场景:ES6 的 Proxy 对象是实现此模式的完美例子。用于:数据验证、访问控制、日志记录、缓存(Memoization)
// const proxy = new Proxy(target, handler);
const target = { msg: 'hi' };
const proxy = new Proxy(target, {
get(obj, prop) {
console.log('获取target.prop:', obj, prop);
return obj[prop];
},
});
console.log('proxy.msg:', proxy.msg);
// 获取target.prop: {msg: 'hi'} msg
// hi
console.log('target.msg:', target.msg);
// hi
proxy.msg = 'hello';
console.log(target.msg, proxy.msg);
// 获取target.prop: {msg: 'hello'} msg
// hello hello
// 缓存代理
function expensiveOperation() {
console.log('This is very expensive!');
// ...复杂计算
return 42;
}
const cache = new Map();
const proxy = new Proxy(expensiveOperation, {
apply(target, thisArg, args) {
const key = args.join('-');
if (cache.has(key)) {
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
proxy(); // 打印 "This is very expensive!",返回 42
proxy(); // 无打印,直接从缓存返回 42
# 行为型模式
# 策略模式
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
策略模式的目的就是将算法的使用和算法的实现分离开来。
一个基于策略模式的程序至少由两部分组成。
第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类 Context(不变),Context 接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明 Context 中要维持对某个策略对象的引用。
/*策略类*/
var levelOBJ = {
A: function(money) {
return money * 4;
},
B: function(money) {
return money * 3;
},
C: function(money) {
return money * 2;
},
};
/*环境类*/
var calculateBouns = function(level, money) {
return levelOBJ[level](money);
};
console.log(calculateBouns("A", 10000)); // 40000
# 观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象(1 对 N),使它们能够自动更新。
观察者模式有一个“别名”,叫发布 - 订阅模式(之所以别名加了引号,是因为两者之间存在着细微的差异)。这个别名非常形象地诠释了观察者模式里两个核心的角色要素——“发布者”与“订阅者”。
例子:一个个开发被产品经理拉群,等需求文档整理好了以后,@所有人 通知各位开发开始“996”。
// 发布者
class Publisher {
constructor() {
this.watchers = [];
console.log("publisher created, watchers is: ", this.watchers);
}
// 添加订阅者
add(watcher) {
this.watchers.push(watcher);
}
// 删除订阅者
remove(watcher) {
this.watchers.forEach((item, index) => {
if (item === watcher) {
this.watchers.splice(index, 1);
}
});
}
// 通知订阅者
notify() {
this.watchers.forEach((watcher) => {
watcher.update(this);
});
}
}
// 订阅者
class Watcher {
constructor() {
console.log("watcher created");
}
update() {
console.log("Watcher.update invoked");
}
}
// 在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。
class PrdPublisher extends Publisher {
constructor() {
super();
// 初始化需求文档
this.prdFile = null;
// 初始化群
this.watchers = [];
console.log("PrdPublisher created");
}
setState(state) {
console.log("PrdPublisher.setState invoked");
// 需求文档发生改变
this.prdFile = state;
// 立刻通知所有开发者
this.notify();
}
getState() {
console.log("PrdPublisher.getState invoked");
return this.prdFile;
}
}
class DeveloperWatcher extends Watcher {
constructor() {
super();
// 还没获取最新需求文档
this.prdFile = null;
console.log("DeveloperWatcher created");
}
update(publisher) {
console.log("DeveloperWatcher.update invoked");
// 更新需求文档
this.prdFile = publisher.getState();
// 开始搬砖
this.work();
}
// 专门搬砖的方法
work() {
// 获取需求文档
const prd = this.prdFile;
// 开始基于需求文档提供的信息搬砖。。。
console.log("996 begins...");
}
}
// 开始了
// 发布者-项目群
const project1 = new PrdPublisher();
// 订阅者
const chieminchan = new DeveloperWatcher("chieminchan");
const kate = new DeveloperWatcher("kate");
const toby = new DeveloperWatcher("toby");
// 项目群拉人
project1.add(chieminchan);
project1.add(kate);
project1.add(toby);
// 更新需求文档
const file = { day1: "需求评审", day2: "梳理需求详细流程" };
project1.setState(file);
# 应用场景
Vue 的双向绑定 、 EventBus/EventEmitter
vue 的双向绑定可以参考另一篇文章,这里展开一下 EventEmitter 的实现。
先来看看应用方式:
const emitter = new EventEmitter();
// 监听sendMsg事件
emitter.on("sendMsg", function(text1) {
console.log("我接收到了消息: ", text1);
});
// 触发emitter事件
emitter.emit("sendMsg", "send test");
console.log("----------------------");
// 一次性监听 forOnce事件
emitter.once("forOnce", function(text1, text2) {
console.log("我接收到了消息: ", text1, text2);
});
emitter.emit("forOnce", "test1", "tets2");
/**
on invoked
我接收到了消息: send test
----------------------
on invoked
我接收到了消息: test1 tets2
off invoked
*/
ES6 实现方式
class EventEmitter {
constructor() {
// 事件与回调函数的映射表
this.handlers = {};
}
// 安装事件监听器
on(event, handler) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[event]) {
this.handlers[event] = [];
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[event].push(handler);
console.log("on invoked");
}
// 移除某个事件回调队列里的指定回调函数
off(event, handler) {
const callbacks = this.handlers[event];
const index = callbacks.indexOf(handler);
if (index > -1) {
callbacks.splice(index, 1);
console.log("off invoked");
}
}
// emit方法用于触发目标事件
emit(evnet, ...args) {
if (this.handlers[evnet]) {
this.handlers[evnet].forEach((handler) => {
handler(...args);
});
}
}
// 为事件注册单次监听器
// 需对回调函数进行包装,使其执行完毕自动被移除
once(event, handler) {
const wrapper = (...args) => {
handler.call(handler, ...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
ES5 实现方式
function EventEmitter() {
this.handlers = {};
}
EventEmitter.prototype.on = function(event, handler) {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event].push(handler);
console.log("on invoked");
};
EventEmitter.prototype.off = function(event, handler) {
const callbacks = this.handlers[event];
const index = callbacks.indexOf(handler);
if (index > -1) {
callbacks.splice(index, 1);
console.log("off invoked");
}
};
EventEmitter.prototype.emit = function(event, ...args) {
if (this.handlers[event]) {
this.handlers[event].forEach((handler) => {
handler(...args);
});
}
};
EventEmitter.prototype.once = function(event, handler) {
const wrapper = (...args) => {
handler(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
};
# 观察者模式与发布-订阅模式的区别是什么
两种的核心思路是一样的,只是规模不同所采取的方案不同而已。
在发布订阅模式里,发布者并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识,通过第三者,也就是在消息队列里面。
有点类似于快递员上门送货,后来快递太多了,为了增加效率,分工更明确一点,现在多了个中间站,菜鸟驿站,快递员方便了,这是在规模起来以后自然而然的选择。现在是人主动去拿快递,如果以后连这也嫌弃效率不高,怎么办?再加一层,菜鸟驿站派出机器人送..
从表面上看:
- 观察者模式里,只有两个角色 —— 观察者 + 被观察者
- 发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— 经纪人 Broker
往更深层次讲:
- 观察者和被观察者,是松耦合的关系
- 发布者和订阅者,则完全不存在耦合
从使用层面上讲:
- 观察者模式,多用于单个应用内部
- 发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern),比如我们常用的消息中间件
参考文章: