【设计模式】常用设计模式

2023/12/04 22:23:392022/12/27 16:38:22

生成器模式

例子:使用Builder模式优化参数极多的函数

单例模式

单例模式要求每次调用都返回同一个对象,惰性单例模式则是只在有调用的情况下才创建单例对象。

例子:登录弹窗。

只在按钮点击的时候创建弹窗的 DOM,之后 createLogLayer 方法返回的都是同一个对象。

  • 单例工厂函数
function singleFactory(fn) {
  let result = null;
  return function () {
    if (!result) {
      result = fn.apply(this, arguments);
    }
    return result;
  };
}
  • 业务逻辑代码
function createLogLayer() {
  var div = document.createElement("div");
  div.innerHTML = "我是登录浮窗";
  div.style.display = "none";
  document.body.appendChild(div);
  return div;
}

var createSingleLoginLayer = singleFactory(createLogLayer);

btn.onclick = () => {
  let loginLayer = createSingleLoginLayer();
  loginLayer.style.display = "block";
};

策略模式

在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量。 ——Peter Norvig

策略模式是指通过封装一系列的算法,根据不同的策略调用对应的算法。

编写可维护的JavaScript 中提出的隔离应用逻辑有些相似。行为是对指令和处理函数进行分离;目的都是提高 策略 也就是具体处理函数的复用性以及可扩展性。

策略模式就是把逻辑中固定的部分和可变的部分分隔开,让我们能够复用那些固定的部分,同时能够灵活地选课可变的部分。

例子:使用策略模式优化表单校验逻辑

代理模式

代理模式可以让目标保持单一职责原则,比如一个求乘积函数应该专注于求乘积,添加缓存的操作可以通过添加一个缓存代理来实现。

代理模式就是通过一个中间对象去访问目标对象,而不是直接操作目标对象。

  • 保护代理:控制不同权限的对象对目标对象的访问。
  • 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。

Vue Store 中对修改数据的操作就是一种保护代理:用户只能通过定义 mutationsactions 方法去修改 state 中的数据。

防抖节流也是一种代理。

例子:使用代理缓存求乘积函数。

// 求乘积函数
function mult() {
  var result = 1;
  for (var i = 0, l = arguments.length; i < l; i++) {
    result = result * arguments[i];
  }
  return result;
}

// 缓存代理函数
var catchProxy = function () {
  var cache = {};
  return function () {
    var args = Array.prototype.join.call(arguments, ",");
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = mult.apply(this, arguments));
  };
};

let proxyMult = catchProxy(mult);

proxyMult(1, 2, 3, 4); // 输出:24
proxyMult(1, 2, 3, 4); // 输出:24

保证代理和本体接口的一致性

对于代理对象的使用者而言,代理接收请求的过程应该是透明的,这样做有两个好处。

  • 用户可以放心地请求代理,他只关心是否能得到想要的结果。
  • 在任何使用本体的地方都可以替换成使用代理。

发布-订阅模式

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。

Vue 的事件总线模式 就是一种发布-订阅模式。

一个简单的观察者类

模块之间如果用了太多的全局发布订阅模式来通信,代码就会变得难以维护,因为依赖并不明确,最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块。

享元模式与对象池

享元模式是一种性能调优的设计模式,主要思路是实现对象共享。

如果一个项目中需要创建非常多的对象,就会占用很多内存,这会导致浏览器性能问题甚至崩溃,如果这些对象有共同性可以尝试将对象共享,减少对象的数量。

对象池也是一种对象共享的方式。这种方式强调将每次创建的对象都缓存起来,如果同时需要三个对象,但是对象池中只有两个,那就需要创建一个新对象,使用完毕后将这个对象也存入对象池中,这时对象池中就有三个对象。

职责链模式

职责链是指将多个处理程序连接到一起,每个处理程序接收同样的参数但是各自实现自己的逻辑。

express 框架的中间件功能就是一种职责链。

例子:下单逻辑

假如有一个下单逻辑,有三种情况:

  1. 500 元定金订单,购买成功后可以获得 100 元优惠券,未支付定金时转入普通购买,不受库存影响。
  2. 300 元定金订单,购买成功可以获得 50 元优惠券,未支付定金时转入普通购买,不受库存影响。
  3. 普通购买,无货时无法购买。

定义三个处理程序,并将其绑定到职责链上:

// orderType 订单类型:1表示500元定金订单、2表示300元定金订单、3表示普通订单。
// pay 定金是否支付。
// stock 是否有货。
function order500(orderType, pay, stock, next) {
  if (orderType === 1 && pay) {
    console.log("购买成功,获得100元优惠券");
  } else {
    next();
  }
}

function order300(orderType, pay, stock, next) {
  if (orderType === 2 && pay) {
    console.log("购买成功,获得50元优惠券");
  } else {
    next();
  }
}

function orderNormal(orderType, pay, stock, next) {
  if (stock) {
    console.log("购买成功");
  } else {
    console.log("购买失败");
  }
}

const orderChain = new Chain();
orderChain.add(order500).add(order300).add(orderNormal); // 在职责链上添加处理程序
orderChain.exec(1, false, true); // 触发职责链执行

Chain 类的实现:

class Chain {
  /**
   * 事件队列
   */
  eventList = [];

  /**
   * 添加处理程序
   * @param {*} fn
   * @returns
   */
  add(fn) {
    if (typeof fn === "function") {
      this.eventList.push(fn);
    }
    return this;
  }

  /**
   * 开始执行
   * @param  {...any} args
   */
  exec(...args) {
    if (this.eventList.length > 0) {
      const event = this.eventList.shift();
      event.call(this, ...args, this.exec.bind(this, ...args));
    }
  }
}

处理程序可以自己决定是否要继续向下传递、何时向下传递,所以异步节点也是支持的:

// ...
function asyncOrder(orderType, pay, stock, next) {
  setTimeout(() => {
    console.log("异步节点执行完毕");
    next();
  }, 1000);
}

// ...
orderChain.add(order500).add(order300).add(asyncOrder).add(orderNormal);
orderChain.exec(1, false, true);

中介者模式

中介者模式可以解决多个对象之前的强耦合关系。

使用中介者模式优化表单交互

状态模式

状态模式是状态机的一种实现,可以提高状态切换逻辑的可维护性。

参考

《JavaScript 设计模式与开发实践》

《Node.js设计模式》