JavaScript 设计模式 —— 策略模式

149 阅读4分钟

如果觉得文章不错,欢迎关注、点赞和分享!

持续分享技术博文,关注微信公众号 👉🏻 前端LeBron

很快,迎来了 JavaScript 设计模式系列的第二篇 —— 策略模式 ...

什么是策略模式

策略模式定义:

定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换

策略模式一般由两部分组成:

  1. 封装不同策略的策略组(使得代码复用性、可扩展、可维护性提高,避免大量 CV 代码的情况)

  2. Context(委托算法,执行策略)

什么时候使用策略模式 ?

策略模式广泛应用于程序研发中,当出现需要根据不同的前置条件执行不同的算法得到结果时,使用策略模式可以让你的代码更加优雅

怎么?不信? 那就上点代码让你感受一下 💩 山的力量!

假设一个函数负责 Consul (服务发现)和 LB (负载均衡)

这里 Consul 函数就是 Context,各种 LB 算法就是策略组

传入服务唯一标识和负载均衡算法

返回服务器实例 IP 地址

如想了解 LB 相关知识,可以看看这篇文章 [深入浅出LB]手把手带你实现一个负载均衡器

function consul(serviceId, algorithm) {
  if (algorithm === 'random') {
    // random flow     
    // ...     
    return 'xxx.xxx.xxx.xxx';
  } else if (algorithm === 'weightedRoundRobin') {
    // weightedRoundRobin flow     
    // ...     
    return 'xxx.xxx.xxx.xxx';
  } else if (algorithm === 'ipHash') {
    // ipHash flow     
    // ...     
    return 'xxx.xxx.xxx.xxx';
  } else if (algorithm === 'urlHash') {
    // urlHash flow     
    // ...     
    return 'xxx.xxx.xxx.xxx';
  } else if (algorithm === 'leastConnection') {
    // leastConnection flow     
    // ...     
    return 'xxx.xxx.xxx.xxx';
  } else if (algorithm === 'fair') {
    // fair flow     
    // ...     
    return 'xxx.xxx.xxx.xxx';
  }
}

可以发现,这段代码存在着一些明显的问题

  1. Consul 函数过于庞大,不符合单一职责原则,难维护

  2. 堆砌了过多的 IF-ELSE 语句,代码看起来比较冗余

  3. 负载均衡算法复用性较差,如果其他地方需要用到...(你不会直接 CV 吧 🤓

针对以上问题,我们可以利用策略模式加以优化

  1. 首先将各 LB 算法封装成独立的函数,提高复用性

  2. 建立算法名称 -> 算法执行函数映射,干掉冗余的 IF-ELSE(简直就是 IF-ELSE 的救世主

  3. 简化 Consul 函数,具体算法对于 Consul 来说是“隐形”的,单一职责

  4. 可拓展性提高,如需拓展更多算法,仅需引入算法和添加 Map 中的配置

function random(serviceId) {
  // random flow   
  // ...   
  return 'xxx.xxx.xxx.xxx';
}
function weightedRoundRobin(serviceId) {
  // weightedRoundRobin flow   
  // ...   
  return 'xxx.xxx.xxx.xxx';
}
function ipHash(serviceId) {
  // IPHash flow   
  // ...   
  return 'xxx.xxx.xxx.xxx';
}
function urlHash(serviceId) {
  // URL Hash flow   
  // ...   
  return 'xxx.xxx.xxx.xxx';
}
function leastConnection(serviceId) {
  // leaseConnection flow   
  // ...   
  return 'xxx.xxx.xxx.xxx';
}
function fair(serviceId) {
  // fair flow   
  // ...   
  return 'xxx.xxx.xxx.xxx';
}

const ALGORITHM = {
  RANDOM: 'random',
  WEIGHTED_ROUND_ROBIN: 'weightedRoundRobin',
  IP_HASH: 'ipHash',
  URL_HASH: 'urlHash',
  LEAST_CONNECTION: 'leastConnection',
  FAIR: 'fair',
};

const ALGORITHM_MAP = {
  [ALGORITHM.RANDOM]: random,
  [ALGORITHM.WEIGHTED_ROUND_ROBIN]: weightedRoundRobin,
  [ALGORITHM.IP_HASH]: ipHash,
  [ALGORITHM.URL_HASH]: urlHash,
  [ALGORITHM.LEAST_CONNECTION]: leastConnection,
  [ALGORITHM.FAIR]: fair,
};

function consul(serviceId, algorithm) {
  return ALGORITHM_MAP[algorithm](serviceId);
}

策略模式应用场景之表单校验

粗糙的表单校验

// 粗糙的表单校验
let loginFrom = document.getElementId("loginFrom")
loginFrom.onsubmit = function (e) {
    const username =  document.getElementId("username")
    const pwd =  document.getElementId("pwd")
    const mobile =  document.getElementId("mobile")
    if (username === '') {
        alert("用户名不可为空")
        return false
    }
     if (pwd.length < 6) {
        alert("密码长度不能少于6位")
        return false
    }
    if (!/^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/.test(mobile)) {
        alert("手机号码格式不正确")
        return false
    }
}

使用策略模式优化后的表单校验

Context:validate

策略组:strategy

// 使用策略模式优化
const strategy = {
  isEmpty: function ({ value }, errMsg) {
    if (value === '') {
      return errMsg;
    }
  },
  isMobile: function ({ value }, errMsg) {
    if (
      !/^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/.test(value)
    ) {
      return errMsg;
    }
  },
  isLongerThanMinLength: function ({ value, len }, errMsg) {
    if (value.length < len) {
      return errMsg;
    }
  },
};

class Validator {
  constructor() {
    this.validateItems = [];
  }

  add(params, strategyKey, errMsg) {
    this.validateItems.push({ params, strategyKey, errMsg });
  }

  start() {
    for (const item of this.validateItems) {
      const { params, strategyKey, errMsg } = item;
      const msg = strategy[strategyKey](params, errMsg);
      if (msg) {
        return msg;
      }
    }
  }
}

function validate() {
  const username = document.getElementId('username');
  const pwd = document.getElementId('pwd');
  const mobile = document.getElementId('mobile');

  const validator = new Validator();
  validator.add({ value: username }, 'isEmpty', '用户名不可为空');
  validator.add(
    { value: pwd, len: 6 },
    'isLongerThanMinLength',
    '密码长度不能少于6位'
  );
  validator.add({ value: mobile }, 'isMobile', '手机号码格式不正确');

  const errMsg = validator.start();
  if (errMsg) {
    alert(errMsg);
  } else {
    alert('校验通过');
  }
}

validate();

策略模式和多态的区别

  • 策略模式定义了算法组,分别封装,它们之间可以相互替换,此模式的变化独立于使用算法的客户

  • 多态常用继承、方法重写、父类引用指向子类对象等方法实现

策略模式强调的是做同一件事的不同且不重复的方法

多态是一种语言机制,有的不支持多态的语言也一样要实现策略模式

策略处于程序设计层次,多态处于语言语法层次

总结

策略模式的优点

  • 策略模式利用组合、委托和多态等技术和思想,可以有效避免多重且冗余的 IF-ELSE

  • 策略模式提供了对开放——封闭原则的完美支持,将算法封装在独立的策略中。可以在不修改原代码的情况下,灵活增加新算法。提高了它们的复用性、和可拓展性,也更容易切换和理解。

  • 策略模式中的算法也可以复用在工程的其他地方,避免大量重复的 CV 工作

  • 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案

策略模式的缺点

  • 策略模式会在程序中增加许多策略函数、类、对象,但实际上比把它们堆砌在 Context 中要更好

  • 使用策略模式必须了解所有的策略,必须了解它们的细节比较它们之间的不同点,才能选择一个合适的策略。此时需要向用户暴露它的所有实现,违反最少知识原则。

设计模式系列文章推荐

持续分享技术博文,欢迎关注!