# 👉 设计模式
设计模式定义:在软件设计中针对不同场景问题的解决方案,使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。
设计模式的核心思想,就是“封装变化”。何为“封装变化”:
这一点相信大家不难理解——如果说我们写一个业务,这个业务是一潭死水,初始版本是 1.0,100 年后还是 1.0,不接受任何迭代和优化,那么这个业务几乎可以随便写。反正只要实现功能就行了,完全不需要考虑可维护性、可扩展性。但在实际开发中,不发生变化的代码可以说是不存在的。
我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定。这个过程,就叫“封装变化”;这样的代码,就是我们所谓的“健壮”的代码,它可以经得起变化的考验。而设计模式出现的意义,就是帮我们写出这样的代码。
设计模式主要有 23 种,分为三大类型为:创建型模式、结构型模式、行为型模式。
这里就不一一详细展开,主要用作简要记录常用的设计模式。
# 创建型模式
# 工厂模式
说到工厂模式之前,看看构造器是什么:
// 场景1:一个公司需要录入很多员工信息
function User(name, age, career) {
this.name = name;
this.age = age;
this.caree = caree;
}
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.caree = caree;
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、自觉非常不爽的情况下,我们也应该思考是不是可以掏出工厂模式重构我们的代码了。比如:手机工厂(抽象出操作系统、硬件芯片、屏幕...)
# 单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。
实现的方法:先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
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);
}
# 结构型模式
# 装饰器模式
装饰器模式,又名装饰者模式。顾名思义,它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。
因此其特征主要有两点:
1) 为对象添加新功能;
2)不改变其原有的结构和功能,即原有功能还继续会用,且场景不会改变。
class Circle {
draw() {
console.log("画一个圆形");
}
}
class Decorator {
constructor(circle) {
this.circle = circle;
}
draw() {
this.circle.draw();
// “包装”了一层新逻辑
this.setRedBorder(circle);
}
setRedBorder(circle) {
console.log("画一个红色边框");
}
}
let circle = new Circle();
let decorator = new Decorator(circle);
decorator.draw(); //画一个圆形,画一个红色边框
该例中,我们写了一个 Decorator 装饰器类,它重写了实例对象的 draw。
如果后续需要对 draw 这个行为添加更多详细对需求,可以直接在 Decorator 中添加。如此一来,我们就可以实现“只添加,不修改”的装饰器模式。
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>'
// 可见,再给属性添加了只读的装饰后,代码试图修改属性的命令将会报错。
# 适配器模式
适配器模式很好理解,在日常开发中其实不经意间就用到了。
适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成适应当前场景的另一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作。所以,适配器模式必须包含目标(Target)、源(Adaptee)和适配器(Adapter)三个角色。
举个例子:
1)日常普通耳机插口是圆形的,换了个新手机 iphone12 手机耳机插口是扁平的 usb-c 接口,想要旧耳机在新手机也能兼容用得上。这个时候转换头就出现了,而转换头就充当着一个适配器的角色...
2)对项目中的 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 封装进了自己复杂的底层逻辑里,暴露给用户的都是十分简单的统一的东西——统一的接口,统一的入参,统一的出参,统一的规则。
# 行为型模式
# 策略模式
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
策略模式的目的就是将算法的使用和算法的实现分离开来。
一个基于策略模式的程序至少由两部分组成。
第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类 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),比如我们常用的消息中间件
参考文章: