【设计模式】让我们的代码更优雅吧~

128 阅读5分钟

前言

个人学习笔记总结汇总,有问题欢迎指正! 其实很多设计模式在我们日常开发中都会被涉猎。好的代码设计能帮我们规避很多不必要产生的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做了两件事:

  1. 执行校验这个动作并返回结果

  2. 执行各种校验算法

问题:

  1. 不满足函数单一功能原则,冗杂了各种校验逻辑。
  2. 相似的校验算法不能合并,比如手机号码和邮箱的正则校验
  3. 灵活性差,更换顺序或者修改部分校验条件都要去代函数validate更改,影响函数的稳定性。
  4. if else的代码平铺,代码冗长,可读性不高。

用策略模式重构以上代码:

  1. 将校验本身功能和各种算法逻辑进行分离
  2. 合并相似校验算法

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(); 
}

使用策略模式优化后的代码:

  1. 避免了过多的if else
  2. 将代码功能进行分隔,并灵活的组装使用
  3. 对于单个策略算法可以单独修改

上述例子还能做些什么?

  1. 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关系即可。

看一个例子:

image.png

假设三个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)

该模式优点:

  1. 时间上的解耦,适用异步编程
  2. 对象之间的解耦

通用实现代码:

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');
}

未完待续...