前言
个人学习笔记总结汇总,有问题欢迎指正! 其实很多设计模式在我们日常开发中都会被涉猎。好的代码设计能帮我们规避很多不必要产生的Bug,也能降低代码的复杂度和维护成本。
1、策略模式
先来看下以下例子:
const data = {};
function vaildate() {
if (!data.name) {
this.$message.error('姓名不能为空');
return false;
} else if (data.name.length > 4) {
this.$message.error('姓名长度不能大于4');
return false;
} else if (!data.age) {
this.$message.error('年龄不能为空');
return false;
} else if (data.age < 0 || data.age > 150) {
this.$message.error('年龄不能小于0岁或着大于150岁');
return false;
} else if (!/(^1[3|5|8][0-9]{9}$)/.test(data.phone)) {
this.$message.error('手机号码格式不正确');
return false;
} else if (!/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/.test(data.email)) {
this.$message.error('邮箱格式不正确');
return false;
}
return true;
}
函数validate做了两件事:
-
执行校验这个动作并返回结果
-
执行各种校验算法
问题:
- 不满足函数单一功能原则,冗杂了各种校验逻辑。
- 相似的校验算法不能合并,比如手机号码和邮箱的正则校验
- 灵活性差,更换顺序或者修改部分校验条件都要去代函数
validate更改,影响函数的稳定性。 - if else的代码平铺,代码冗长,可读性不高。
用策略模式重构以上代码:
- 将校验本身功能和各种算法逻辑进行分离
- 合并相似校验算法
vaildate-utils.js
// 校验策略
const strategies = {
// 校验是否为空
isEmpty(value, errorMsg) {
if (!value) {
return errorMsg;
}
},
// 校验最大长度
maxLength(value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
// 校验是否在范围内
inRange(value, range = [], errorMsg) {
if (value < range[0] || value > range[1]) {
return errorMsg;
}
},
// 校验正则
regTest(value, reg, errorMsg) {
if (!reg.test(value)) {
return false;
}
}
};
class Validator {
constructor() {
this.validateFns = [];
}
add(value, rules, errorMsg) {
// 可补充一些容错校验
const ruleName = rules[0];
this.validateFns.push(() => {
return strategies[ruleName](value, ...rules.slice[1], errorMsg);
});
}
run() {
for (let i = 0, validateFn; validateFn = this.validateFns[i++];) {
const errorMsg = validateFn();
if (errorMsg) {
return { result: false, errorMsg };
}
}
return { result: true };
}
}
export default Validator;// 校验策略
使用上述类Validator,重构校验逻辑。
import Validator from 'vaildate-utils';
const data = {};
const validate = () {
const validator = new Validator();
// 增加校验规则
validator.add(data.name, ['isEmpty'], '姓名不能为空');
validator.add(data.name, ['maxLength', 4], '姓名长度不能大于4');
validator.add(data.age, ['isEmpty'], '年龄不能为空');
validator.add(data.age, ['inRange', 0, 150], '年龄不能小于0岁或着大于150岁');
validator.add(data.phone, ['regTest', /(^1[3|5|8][0-9]{9}$)/], '手机号码格式不正确');
validator.add(data.email, ['regTest', /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/], '邮箱格式不正确');
// 获取校验结果和错误提示
const { result, errorMsg } = validator.run();
}
使用策略模式优化后的代码:
- 避免了过多的if else
- 将代码功能进行分隔,并灵活的组装使用
- 对于单个策略算法可以单独修改
上述例子还能做些什么?
- strategies的方法中可以进行更加细致的类型校验,或者内置一些默认信息。比如通过在业务代码中会比较粗略进行类型判断,将代码抽个独立的函数后,可以将代码写的更加严谨。
// 校验最大长度
const maxLength = (value, length, key) => {
const errorMsg = `${key}长度不能大于${length}`;
// 进行严谨的类型判断
if (typeof value !== 'string' || typeof length !== 'number') throw('maxLength: 参数类型不正确');
if (value.length > length) {
return errorMsg;
}
},
// 内置唯一的正则表达式,统一维护管理
const regMaps = {
phone: /(^1[3|5|8][0-9]{9}$)/,
email: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/
}
// 校验正则
// 进行比较完备的类型判断 避免调用方法时报错
const regTest = (value, reg, errorMsg) => {
if (typeof reg === 'string' && Object.keys(regMaps).includes(reg)) {
reg = regMaps[reg];
}
if (typeof reg !== 'regexp') throw('regTest: 参数类型不正确')
if (!reg.test(value)) {
return false;
}
}
// 使用
validator.add(data.email, ['regTest', 'email'], '邮箱格式不正确');
2、单例模式
该模式的定义是保证一个类仅有一个实例,并且提供一个访问它的全局访问点。该访问点有以下特点:
- 惰性:使用时创建
- 会通过某个变量来标志该类是否创建过实例对象
看以下例子了解单例模式的基本实现。
class Singleton {
constructor(name) {
this.name = name;
this.init();
}
// 全局访问点
static getInstance(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
init() {
console.log(this.name);
}
}
// 标志: 判断是否创建过实例对象
Singleton.instance = null;
const a = Singleton.getInstance('a');
const b = Singleton.getInstance('b');
console.log(a === b);
缺点:这个类的职责 自身类的功能 + 管理唯一实例功能 不符合单一职责原则,即这个类又需要实现自身的逻辑,又要保证实例的唯一性。
增强版例子: 我们通过增加一个基类来管理唯一实例
class Singleton {
constructor(name) {
this.name = name;
this.init();
}
init() {
console.log(this.name);
}
}
// 代理类
class ProxySingleton {
constructor(name) {
if (!ProxySingleton.instance) {
ProxySingleton.instance = new Singleton(name);
}
return ProxySingleton.instance;
}
}
ProxySingleton.instance = null;
const a = new ProxySingleton('a');
const b = new ProxySingleton('b');
console.log(a === b);
javascript中应用单例模式
在js中,我们可以把类进行弱化,确保一个实例可以不单单指类得实例,也可以是一个唯一得对象。比如全局对象,但是要注意它的缺点:容易造成命名空间的污染,也容易被覆盖。但是可以使用命名空间或者闭包进行处理。
3、代理模式
代理模式的核心即控制对对象的访问,通常会为对象提供一个代理对象。
根据代理对象的作用,又可以将代理模式进行细分:
1.保护代理(不常用)
作用:过滤访问者,保护代理对象。
一般会根据访问者的条件来过滤,当满足一定条件时才可以访问目标对象。但是由于在js中无法知道是谁访问了对象,所以保护代理很难实现。
2.虚拟代理(常用)
代理对象与目标对象的接口一致
- 使目标对象专注自身的功能实现
- 代理对象去无感处理一些额外的事情
- 符合单一职责原则
👇一个简单的例子:
// 目标对象
const image = (function(){
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return function(src) {
imgNode.src = src;
}
})()
// 代理对象
const proxyImage = (function(){
const img = new Image();
img.onLoad = function() {
image(this.src);
}
return function(src) {
image('占位图片地址');
img.src = src;
}
})()
proxyImage('图片地址');
// 若不需要使用proxyImage的预加载图片地址直接访问目标对象即可
// image('图片地址');
JS中的Proxy也是一种虚拟代理。
var targetObj = {};
var handler = {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
// 处理异常赋值
if (!Array.isArray(value)) value = [];
return Reflect.set(target, propKey, value, receiver);
}
};
var proxyObj = new Proxy(target, handler);
proxyObj.a = 'b';
proxyObj.a // "b"
- 统一的访问接口:get set实现了targetObj的基本访问
proxyObj.a === targetObj.a - 可以在代理对象中实现其他实现功能
3.缓存代理
作用:为一些开销较大的运算结果提供暂时的存储。
// 乘积
const mult = (...args) => {
return args.reduce((res, item) => res * item, 1);
}
// 加和
const plus = (...args) => {
return args.reduce((res, item) => res * item, 1)
}
const createProxyFactory = function(fn) {
const cache = {};
return function(..._args) {
const args = _args.join(',');
if (args in cache) return cache[args];
return cache[args] = fn(..._args);
}
}
const proxyMult = createProxyFactory(mult);
const proxyPlus = createProxyFactory(plus);
proxyMult(1, 2, 3, 4);
proxyMult(1, 2, 3, 4);
proxyPlus(1, 2, 3, 4);
proxyPlus(1, 2, 3, 4);
4、中介者模式
当以下四个对象互相引用时,当我们修改任意一个对象时,都需要去通知与A相关联的对象。
缺点:对象应用关系难以维护,牵一发而动全身。
中介者模式的作用就是减少对象与对象之间的紧耦关系。
增加一个中介者,作为对象之间的通信员,来处理对象与对象之间的信息传递。
对象只需要保持和中介者对象的1对1关系即可。
看一个例子:
假设三个select查询,从左至右分别为查询1 查询2 查询3:
- 查询2的数据请求需要依赖查询1的值
- 查询3的数据请求需要依赖查询1和查询2的值
const data = {}
const updateModel = (key, val) => {
data[key] = val;
}
const broadcast = (key) {
// 遍历表单项
for(/* select list */) {
// 判断依赖关系
if(depChange){
loadData();
}
}
}
// 在select change的回调方法中统一通过broadcast方法去通知其他对象
const handleSelectChange(key, val) => {
updateModel(key, val);
badcast.call(key);
}
5、发布订阅模式(观察者模式)
对象间的一对多的依赖关系,当对象发生更改时,依赖该对象的所有对象都会被通知。
现实中的例子: 比如最近要开卖一个商品,想去买的人都去订阅这个商品的开售信息,并在这个商家这里留下了自己的手机号码,当这个商品到货开卖了,商家就会打电话给想买商品的人。
DOM事件也是很典型的发布订阅模式:
// 订阅document.body的click事件
document.body.addEventListener('click', fn1)
// 多个对象的订阅
document.body.addEventListener('click', fn2)
// 发布通知
document.body.click();
// 删除订阅
document.body.removeEventListener('click', fn2)
该模式优点:
- 时间上的解耦,适用异步编程
- 对象之间的解耦
通用实现代码:
const _map = Symbol('map');
const _on = Symbol('on');
class EventEimtter {
// 用下划线暂时区别的类的公用方法和私有方法
[_on](eventName, fn, once) {
if (this[_map].has(eventName)) {
this[_map].get(eventName).fn.push(fn);
} else {
this[_map].set(eventName, { fn: [fn], once })
}
}
constructor() {
this[_map] = new Map();
}
on(eventName, fn) {
this[_on](eventName, fn, false);
}
once(eventName, fn) {
this[_on](eventName, fn, true);
}
off(eventName) {
this[_map].delete(eventName);
}
emit(eventName, ...args) {
const { fn, once } = this[_map].get(eventName) || {};
// 无对应的订阅回调事件,返回false
if (!fn) {
return false;
}
// 依次执行回调函数
for (let i = 0; i < fn.length; i++) {
fn[i](...args);
}
if (once) {
this.off(eventName);
}
return true;
}
}
const events = new EventEimtter();
// 订阅event1事件
events.on('event1', (a, b) => {
console.log(a + b);
});
// 订阅event1事件
events.on('event1', (a, b) => {
console.log(a * b);
});
// 订阅onceEvent事件
events.once('onceEvent', (a, b) => {
console.log(a + b);
});
// 发布event1事件和onceEvent事件
events.emit('event1', 2 , 3);
events.emit('onceEvent', 2 , 2);
// 取消订阅 event1 event1
events.off('event1');
events.off('onceEvent');
用发布-订阅实现中介者模式,还是上述中介者模式的例子,表单项互相依赖,作为数据请求的参数。
const events = new EventEimtter();
// 请求数据
const loadData =(key)=> {}
// 遍历表单项
for(/* select list */) {
// 判断依赖关系
if(depChange){
// 若存在依赖关系,订阅broadcast事件
events.on('broadcast', () => loadData(key));
}
}
// select change
const handleSelectChange = (key, val) => {
updateModel(prop, val);
events.$emit('broadcast');
}
未完待续...