JavaScript设计模式之策略模式

5,124 阅读7分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

概念

在《JavaScript设计模式与开发实践》 中对策略模式的定义为:定义一系列算法,把他们一个个封装起来,并且使他们可以相互替换。在平时的工作中也存在非常多应用场景,比如业务中经常会存在针对不同场景执行不同逻辑的情况,就可以考虑使用策略模式

策略模式

策略模式应用

举一个书中的例子来介绍如何使用策略模式重构代码。
业务描述:实现一个计算年终奖的功能,绩效为S的人有4倍工资A3倍工资B2倍工资

最初代码

通常这类功能我们会写出如下代码:

var calculateBouns = function (performanceLevel, salary) {
    if (performanceLevel === 'S') {
        return salary * 4;
    }
    
    if (performanceLevel === 'A') {
        return salary * 3;
    }
    
    if (performanceLevel === 'B') {
        return salary * 2;
    }
}

calculateBouns('S', 20000);

这类的代码相信大家工作中肯定是没有少写的,虽然现在看着很清晰,但是会存在哪些问题呢?

  • 包含过多的if-else语句;
  • 可扩展性较差,如果我们需要扩展一个新的绩效C,就需要深入到calculateBouns内部实现,违反了开闭原则;
  • 算法复用性差,如果需要复用这段算法,只能选择复制黏贴

使用策略模式重构代码

使用策略模式的目的就是将算法的使用与算法的实现分离开来。显然我们需要把 算法实现(绩效算法)算法使用(计算年终奖) 分离开来,代码如下:

// 绩效算法
var performanceS = function() {};
performanceS.prototype.calculate = function(salary) {
    return salary * 4;
}

var performanceA = function() {};
performanceA.prototype.calculate = function(salary) {
    return salary * 3;
}

var performanceB = function() {};
performanceB.prototype.calculate = function(salary) {
    return salary * 2;
}

// 计算年终奖
var Bouns = function() {
    this.salary = null; // 原始工资
    this.strategy = null; // 绩效等级对应的策略对象
}

Bouns.prototype.setSalary = function(salary) {
    this.salary = salary;
}
Bouns.prototype.setStrategy = function(strategy) {
    this.strategy = strategy;
}
Bouns.prototype.getBouns = function() {
     return this.strategy.calculate(this.salary);
}

var bouns = new Bouns()
bouns.setSalary(1000); // 设置工资
bouns.setStrategy(new performanceS()); // 设置绩效
bouns.getBouns(); // 获取奖金

用这种方式重构后的代码虽然将不同逻辑分开,结构更加清晰。不过个人感觉有点小题大做了,如果这是真实的业务需求,我肯定不会这样重构代码,不过举这样简单的例子,也只是提供一种思路而已,当遇到足够复杂的算法时,使用这种方式也是可以考虑的。

JavaScript版本的策略模式

实际上在JavaScript语言中,函数也是对象,所以更简单和直接的做法是把Strategy直接定义为函数:

var strategies = {
    'S': function(salary) {
        return salary * 4;
    },
    'A': function(salary) {
        return salary * 3;
    },
    'B': function() {
        return salary * 2;
    }
}

var calculateBouns = function(level, salary) {
    return strategies[level](salary);
}

calculateBouns('A', 2000);

这种方式重构后的代码看着会更简洁一些,也更符合在实际业务中使用。

源码中的策略模式

上面介绍了策略模式的概念,并通过一个例子对策略模式的使用场景和编码思路有了更深刻的理解。策略模式在很多场景中都是非常实用的,接下来我将介绍几个开源项目中用到的场景,来看下这些大佬们是如何使用策略模式的吧。

Axios

众所周知,axios既可用于浏览器中又可用于node环境中,但通过源码可以得知:在不同的环境,将使用不同的方式发起请求,那么axios是如何处理的呢?

// lib/defaults.js

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

通过这段代码,可以看到在axios中,我们会根据环境赋值不同的adapter,但XHRHttp发送请求的方式并不相同,那么如何保证在不同场景使用方式相同呢?其实,axios会将不同的逻辑在各自内部处理,最终暴露出相同的调用方式,简单看下以下两部分代码:

// lib/adapters/xhr.js

module.exports = function xhrAdapter(config) {
    return new Promise((resolve, reject) => {
        /**
            省略xxxx代码
        */
        var request = new XMLHttpRequest();
        
        /**
            省略xxxx代码
        */
        request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    })
    
    request.onreadystatechange = function handleLoad() {
        /**
            省略xxxx代码
        */
    
        // 对response进行校验,满足条件则请求成功 resolve(response)
        settle(resolve, reject, response);
    }
}


// lib/adapters/http.js

module.exports = function httpAdapter(config) {
    return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
        var resolve = function resolve(value) {
          resolvePromise(value);
        };
        var reject = function reject(value) {
          rejectPromise(value);
        };
        
        /**
            省略xxxx代码
        */
       var transport;
       // 源码中有不同逻辑的判断,这里简化为其中一种情况
       transport = isHttpsProxy ? https : http;
       
       /**
            省略xxxx代码
        */
       
       var req = transport.request(options, function handleResponse(res) {
           /**
               省略xxxx代码
           */
           res.on('end', function handleStreamEnd() {
              /**
                省略xxxx代码
              */
              settle(resolve, reject, response);
            });
       })
    })
}

两个都返回Promise,在不同的方法中,各自处理了响应逻辑。在于使用时,也就不需要再区分不同的环境了。

通过学习Axios中的策略模式的应用,我对这种思路有了更深刻的印象:当需要根据不同的场景选择不同策略时,我们也需要将有区别的地方,都封装在各自的策略中,而在使用的时候,只调用固定的方法即可。这种方式给我的感觉有点像前面计算绩效那个例子中面向对象的重构方式,对每个策略下都封装了calculate方法,获取不同的绩效的奖金都只调用calculate即可,再看axios这个例子,当每种策略的逻辑实现差别较大或不同点较多时,考虑面相对象的封装方式处理不同策略会显得更加清晰,这只是我的理解分享给大家,如果大家有不同的想法也欢迎一起讨论

策略模式应用

策略模式的应用场景还是比较多的,通常可以用在多种校验规则封装、js不同动画效果封装等场景中。在实际的工作中,我也有用到过策略模式来重构了部分代码,先说场景:
由于业务中支持了一种新的客户类型(假如是VIP客户),同时,针对VIP客户的数据获取后端提供了新的接口,这样的话就会导致一个问题,前端很多数据请求都需要去根据客户类型(VIP或普通客户)请求不同的接口。
如果是以前的做法,可能会真的在每个请求处都加一个if-else,在掌握了策略模式并且学习了Axios源码中的思路,我也做了一个很好的优化,代码如下:

class VIPClient {
    constructor(clientId) { 
        this.clientId = clientId; 
    }
    
    getList() {
        // 获取列表接口
        return getVIPList(this.id)
    }
    
    getDetail() {
        // 获取详情接口
        return getVIPDetail(this.id)
    }
}

class Client {
    constructor(clientId) { 
        this.clientId = clientId; 
    }
    
    getList() {
        // 获取列表接口
        return getList(this.id)
    }
    
    getDetail() {
        // 获取详情接口
        return getDetail(this.id)
    }
}

let client;
if (vip) {
    client = new VIPClient(id)
} else {
    client = new Client(id)
}

// 获取列表
const list = await client.getList()

以上是我将实际开发中的代码简化后的版本,这个思路也是我在学习策略模式时想到的优化方式。个人感觉这个重构价值还挺大的,不论是代码的清晰度还是后期维护,都比之前的if-else方式好了太多了。学会这个思路真的太棒了~~

总结

策略模式在书中其实也没有特别多的介绍,可能也是因为足够简单易于理解。但我认为策略模式在平时的开发中尤其是业务开发中有着很多的使用场景,所以介绍了一个收获较大的Axios源码中的场景,并通过不同客户类型的例子将我的思路和理解分享出来,也希望小伙伴们能将一些自己的想法分享出来一起讨论。
感谢阅读 🙏