JS常用设计模式解析02-策略模式

1.简介

在于都本文之前,希望大家能够先阅读以下JS进阶系列03-JS面向对象的三大特征之多态这篇文章,了解JS的多态。在这篇文章,我们举了一个例子,就是选拔官员选拔合唱团成员时,他并不需要提前知道所有的成员在唱歌时具体会发出什么声音。他关注的只是,他发出命令“唱”时,合唱团成员就会开始唱歌。至于每个成员具体唱什么,交给他们自己好了。 这其实就是一个典型的策略模式,当我们在定义一个方法时,如果涉及到了太多的条件分支时,就应该思考一下,这些分支有没有必要定义在这个方法中。更准确地说,这个方法是不是需要提前知道所有的规则,这些规则是不是固定不会改变的。如果答案是否,那么你可以考虑将这些具体的规则剥离出来,交给传入的参数去实现,方法主体只需要关注你不变的目的即可。 策略模式的定义是:定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换。不过实际业务中,策略模式并不只是封装算法,如果一系列业务规则指向目标一致,并且可以被互相替换使用,我们都可以用策略模式来封装它们。下面我们举几个策略模式的使用场景,让大家详细体会一下。

2. 使用策略模式计算奖金

试想如下场景:公司hr需要你来为其编写一段代码,用来计算年底为每个员工发放的奖金。规则是:考核A的员工发五倍月工资,考核B的员工发三倍月工资,考核C的员工只有一个月工资。你很自然会写出如下代码:

function calculateBonus(level, salary) {
    if(level === 'A') {
        return salary * 5;
    } else if (level === 'B') {
        return salary * 3;
    } else if (level === 'C') {
        return salary;
    }
}

calculateBonus('A', 10000);  // 50000

这个函数接收两个参数,考核等级level和月工资salary,按照不同的等级匹配不同的奖金计算规则并返回。这要求我们在函数中将所有现阶段可能出现的规则都列出来,并且当未来对规则有删减或者改动时,都需要重新修改该方法逻辑,这样的方法在扩展性和可维护性上显然是不好的。

我们考虑一下对这段代码进行改写,我们的目的是为了让hr一调用该方法就能正确输出奖金数目,而具体如何产生奖金,这个方法并不需要去关心。所以我们可以将不变的目的隔离出来,将可变的计算奖金规则封装起来。如下:

function levelA() {}
levelA.prototype.calculate = function(salary) {
    return salary * 5;
};
function levelB() {}
levelB.prototype.calculate = function(salary) {
    return salary * 3;
};
function Bonus(salary, strategy) {
    this.salary = salary;
    this.strategy = strategy;
}
Bonus.prototype.getBonus = function() {
    return this.strategy.calculate(this.salary);
};

var bonus = new Bonus(10000, new levelA);
bonus.getBonus();  // 50000

我们最终需要使用的是Bonus类,只需要向其传入salary,和考核人员所属的类别,然后调用其getBonus() 方法即可。这个是典型的模仿传统的面向对象的实现方式,JS是无类的,其实现方式更为简单。

var strategies = {
    'A': function(salary) {
        return salary * 5;
    },
    'B': function(salary) {
        return salary * 3;
    },
    'C': function(salary) {
        return salary;
    }
};
function caculateBonus(salary, level) {
    return strategies['level'](salary);
}

caculateBonus(10000, 'A');  // 50000

3. 使用策略模式实现表单校验

表单校验是一个很常见的需求,假设你需要为一个网站编写注册模块。用户需要输入用户名,密码和手机号以后点击注册按钮进行注册,在向后台发起请求前,需要在前端校验客户输入的合法性:用户名不能为空,密码长度不能少于6位,手机号码必须符合格式。 先来看第一版实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>表单校验01</title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName"/>
    请输入密码:<input type="text" name="passWord"/>
    请输入手机号码:<input type="text" name="phoneNumber">
    <button>提交</button>
</form>
<script>
    var registerForm = document.getElementById('registerForm');
    registerForm.onsubmit = function () {
        if (registerForm.userName.value === '') {
            alert('用户名不能为空');
            return false;
        }
        if (registerForm.passWord.value.length < 6) {
            alert('密码不能少于6位');
            return false;
        }
        if (!/^1[3|5|8][0-9]{9}$/.test(registerForm.phoneNumber.value)) {
            alert('手机号格式不正确');
            return false;
        }
    };
</script>
</body>
</html>

这个版本在逻辑上是最容易想到的,但是其和计算奖金的最初版本拥有一样的缺点:

  1. registerForm.onsubmit 函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的校验规则。
  2. registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的校验长度从6改为8,我们都必须深入registerForm.onsubmit 函数的内部实现,这是违反开放-封闭原则的。
  3. 算法的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的校验,那么我们很可能随处都可见这些校验逻辑规则的复制。

下面,我们使用策略模式来实现表单校验。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>表单校验02</title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName"/>
    请输入密码:<input type="text" name="passWord"/>
    请输入手机号码:<input type="text" name="phoneNumber">
    <button>提交</button>
</form>
<script>
    // 封装策略对象
    var strageties = {
        isNonEmpty: function (value, errMsg) {
            if (value === '') {
                return errMsg;
            }
        },
        minLength: function (value, length, errMsg) {
            if (value.length < length) {
                return errMsg;
            }
        },
        isMobile: function (value, errMsg) {
            if(!/^1[3|5|8][0-9]{9}$/.test(value)) {
                return errMsg;
            }
        }
    };

    // 校验规则
    function Validate() {
        this.cache = [];    // 保存校验规则
    }
    Validate.prototype.add = function (dom, rule, errMsg) {
        var array = rule.split(':');    //获取用户通过key:value指定的规则
        this.cache.push(function () {   // 把校验步骤用空函数包起来,并依次放入cache
            var stragety = array.shift();   // 取出用户指定的stragety
            array.unshift(dom.value);   // 将input的value放到参数列表最前面
            array.push(errMsg); //  将errMsg放到参数列表最后面
            return strageties[stragety].apply(dom, array);  // 实际执行时传给指定stragety的参数为[dom.value,rule.value,errMsg]
        })
    };
    Validate.prototype.start = function () {
        for (var i=0,validateFunc; validateFunc = this.cache[i++];) {
            var errMsg = validateFunc();
            if (errMsg) {
                return errMsg;
            }
        }
    };

    var registerForm = document.getElementById('registerForm');
    var validateFunc = function() {
        var validate = new Validate();
        validate.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
        validate.add(registerForm.passWord, 'minLength:6', '密码长度不小于6位');
        validate.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');

        var errMsg = validate.start();
        return errMsg;
    };

    registerForm.onsubmit = function () {
        var errMsg = validateFunc();
        if (errMsg) {
            alert(errMsg);
            return false;
        }
    };
</script>
</body>
</html>

这样改写代码以后,如果我们需要为某个输入框加入指定规则,只需要调用add方法即可,灵活且便与扩展。美中不足的是,如果我们要为一个输入框添加多个规则时,需要重复调用多次add。那么有没有办法,只用调用一次add就可以为输入框添加多种规则呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>表单校验03</title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName"/>
    请输入密码:<input type="text" name="passWord"/>
    请输入手机号码:<input type="text" name="phoneNumber">
    <button>提交</button>
</form>
<script>
    // 封装策略对象
    var strageties = {
        isNonEmpty: function (value, errMsg) {
            if (value === '') {
                return errMsg;
            }
        },
        minLength: function (value, length, errMsg) {
            if (value.length < length) {
                return errMsg;
            }
        },
        isMobile: function (value, errMsg) {
            if(!/^1[3|5|8][0-9]{9}$/.test(value)) {
                return errMsg;
            }
        }
    };

    // 校验规则
    function Validate() {
        this.cache = [];    // 保存校验规则
    }
    Validate.prototype.add = function (dom, rules) {
        var self = this;
        for(var i = 0,rule; rule=rules[i++];) {
            (function(rule){
                var array = rule.stragety.split(':');
                var errMsg = rule.errMsg;
                self.cache.push(function () {
                    var stragety = array.shift();   // 取出用户指定的stragety
                    array.unshift(dom.value);   // 将input的value放到参数列表最前面
                    array.push(errMsg); //  将errMsg放到参数列表最后面
                    return strageties[stragety].apply(dom, array);  // 实际执行时传给指定stragety的参数为[dom.value,rule.value,errMsg]
                })
            })(rule)
        }
    };
    Validate.prototype.start = function () {
        for (var i=0,validateFunc; validateFunc = this.cache[i++];) {
            var errMsg = validateFunc();
            if (errMsg) {
                return errMsg;
            }
        }
    };

    var registerForm = document.getElementById('registerForm');
    var validateFunc = function() {
        var validate = new Validate();
        validate.add(registerForm.userName, [{stragety: 'isNonEmpty',errMsg: '用户名不能为空'},{stragety:'minLength:6', errMsg: '用户名长度不能小于6'}]);
        validate.add(registerForm.passWord, [{stragety:'minLength:6', errMsg: '密码长度不小于6位'}]);
        validate.add(registerForm.phoneNumber,[{stragety: 'isMobile', errMsg: '手机号码格式不正确'}]);

        var errMsg = validate.start();
        return errMsg;
    };

    registerForm.onsubmit = function () {
        var errMsg = validateFunc();
        if (errMsg) {
            alert(errMsg);
            return false;
        }
    };
</script>
</body>
</html>

4. 动态类型下的策略模式

我们说过,JS是动态类型的,函数接受的参数并没有限制类型,所以,我们其实不必要把策略都封装在一个对象中。比如,计算bonus的题目,我们可以这样修改:

var A = function(salary) {
        return salary * 5;
    },
var B = function(salary) {
        return salary * 3;
    },
var C = function(salary) {
        return salary;
    }
function caculateBonus(salary, level) {
    return strategies['level'](salary);
}

caculateBonus(10000, 'A');  // 50000

5. 策略模式的优缺点和使用

策略模式的优点:

  1. 策略模式利用组合,委托和多态等技术思想,可以避免多重条件语句。
  2. 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的stragety中,使得它们易于切换,理解和扩展。
  3. 策略模式中算法也可以在其他地方复用,避免冗余代码。
  4. 策略模式利用组合和委托是的Context具有执行算法的能力,这也是继承一种更轻便的替代方案。

策略模式的缺点:

  1. 使用策略模式会增加许多策略类或者策略对象。不过这要比将它们的逻辑堆砌在Context要好。
  2. 要使用stragety,必须要了解所有stragety的细节。此时stragety向客户暴露了其实现,这是有违最少知识原则的。

总体来说,使用策略模式来消除众多的条件分支是利大于弊的。在JS中,使用策略模式有时是隐形的,不必要将策略放在特殊的类或者对象中,其策略往往是一个个单独的函数。合理选用策略模式,会让我们的代码更加灵活且易于扩展。

参考

BOOK-《JavaScript设计模式与开发实践》 第5章

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏walterlv - 吕毅的博客

合并 Jekyll 多种类型的页面

发布于 2017-10-12 15:48 更新于 2018-08...

1032
来自专栏游戏杂谈

php正则表达式的分组捕获

经过测试,发现php正则表达式获取分组捕获是从$0开始,而平时工作中JavaScript中的正则是$1..$9

1493
来自专栏Crossin的编程教室

一些常见的新手问题

本账号开设至今,回答的各种问题,没有一万也有八千了。今天挑了其中曝光率较高的一些问题,跟各位分享,供入门不久和新关注的同学参考。 1. 我是零基础,如何开始学习...

3059
来自专栏吉浦迅科技

DAY41:阅读Synchronization Functions

waits until all threads in the thread block have reached this point and all glob...

843
来自专栏LuckQI

Java核心技术讲解学习

1442
来自专栏Python中文社区

Python 3.7.0 版本正式发布,新特性来袭

Python 3.7.0版本已经于美国时间2018年6月27日发布,同时发布的也有Python 3.6.6版本,下面是来自Python官网的通知:

1424
来自专栏UDNZ

【译】Go 语言实践:编写可维护的程序的建议

本文为 QCon 2018 上海站主题演讲嘉宾、Heptio 资深工程师、著名 Go 语言专家 David Cheney 关于 Go 语言实践的英文分享。为方便...

5128
来自专栏程序员的SOD蜜

TDD(测试驱动设计):通过大量测试寻找最优解决方案

 这两天,我一直在做“测试人员”,不过跟一般的测试人员不同的是,我是在写代码做测试,这些代码是我头脑中的某种设计理念的表示,我坚信,只有不断的“测试”我的这些...

2237
来自专栏优启梦

渗透测试中的域名伪装原理

今天在《网络渗透测试--保护网络安全的技术、工具、过程》一书中看到了一个关于对恶意链接进行域名伪装的方法,以前从不知道的一个方法,特此记录下来:

2095
来自专栏林冠宏的技术文章

php isset( $test ) 的神奇之处。

这次总结下 php 的一个 函数 :  boolean isset($test), 返回值:boolean类型,传入参数不为空,返回true,反之,false ...

19310

扫码关注云+社区

领取腾讯云代金券