Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端必会设计模式之(三) 策略模式 #6

Open
LyzSg opened this issue Jun 12, 2018 · 0 comments
Open

前端必会设计模式之(三) 策略模式 #6

LyzSg opened this issue Jun 12, 2018 · 0 comments

Comments

@LyzSg
Copy link
Owner

LyzSg commented Jun 12, 2018

先来看一个简单的表单验证场景,我们先限定一些规则:用户名不能为空且长度不能超过5,密码不能为空且长度不能小于6,当点击登录按钮时开始验证,验证通过则发送请求。

接着来实现它:

<div>
    用户名:<input type="text" id="userDom" name="username">
    <span id="showUser"></span><br>
    密码:<input type="password" id="psDom" name="password">
    <span id="showPs"></span><br>
    <button id="loginDom">login</button>
</div>
<script>
var flag;
loginDom.onclick = function () {
    flag = true;
    if(userDom.value == '') {
        showUser.innerText = '用户名不能为空';
        flag = false;
    } else if(userDom.value.length > 5) {
        showUser.innerText = '用户名长度不能超过5';
        flag = false;
    }
    if(psDom.value == '') {
        showPs.innerText = '密码不能为空';
        flag = false;
    } else if(psDom.value.length < 6) {
        showPs.innerText = '密码长度不能少于6';
        flag = false;
    }
    if(flag) {
        myRequest({username: userDom.value, password: psDom.value}, 'xxx', 'POST', deal);
    }
}
// 发送请求
function myRequest( data, url,type, cb) {
    $.ajax({
        type: type,
        url: url,
        data: data,
        success: function (data) {
            cb(data)
        }
    })
}
// 处理数据
function deal() {
    // deal data
}
</scipt>

image

这样一个简单的表单校验功能就实现了。但是这样好不好呢?这又和我们的策略模式有什么关系呢?

如果需求仅仅是上述那样,这样写已经基本足够了,使用设计模式反而会复杂化,增加不必要的工作量。

但是,如果我们的需求特别多,需要校验的东西特别多(例如一个发布页),这样就不合适了,缺点:

绑定的函数比较庞大,包含了很多的if-else语句,看着都恶心,这些语句需要覆盖所有的校验规则。

绑定的函数缺乏弹性,如果增加了一种新的校验规则,或者想要把密码的长度校验从6改成8,我们都必须深入绑定的函数的内部实现,这是违反了开放-封闭原则的。

算法的复用性差,如果程序中增加了另一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野。

利用策略模式,结合安全代理,实现复杂表单验证

策略模式(Strategy Pattern)是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。

优点:策略模式提供了管理相关的算法族的办法、策略模式提供了可以替换继承关系的办法、使用策略模式可以避免使用多重条件转移语句。

在重构这个表单验证之前,我们也把代理模式结合在一起,使用安全代理结合策略模式去实现。安全代理就是要过滤一些不安全的请求,所以比较适用于这里的表单验证。

一个大致框架:

loginDom.onclick = function () {
    ProxyRequest({username: userDom.value, password: psDom.value}, 'xxx', 'POST', deal);
}
function myRequest( data, url,type, cb) {
    $.ajax({
        type: type,
        url: url,
        data: data,
        success: function (data) {
            cb(data)
        }
    })
}
function deal() {
    // deal data
}
var ProxyRequest = (function () {

    /*  策略模式的内容,表单验证,拒绝if - else */

    return function () {
        var args = arguments;
        if(flag) {
            myRequest.apply(this, args);
        }
    }
}());

首先,我们要明确策略模式需要些什么来实现。

  1. 首先需要一个用于校验的类Validator,用他来创建一个对象。
  2. Validator类的原型上,有一个strategies对象,他是一个策略(算法)的集合,我们通过这些策略来进行真正的校验。
  3. Validator类的原型上,有一个add方法,用来给某个DOM添加校验规则,这些规则和strategies里的方法相对应。也就是说strategies里必须有这个规则才行。
  4. 原型上还有一个start方法,用来开始我们全部的验证。
  5. 还有一个extend方法,用来给用户提供一个接口,来扩展strategies。

接下来我们具体来规定一下这个add方法需要传什么参数:
首先他接收3个参数,第一个是dom,表示给谁进行校验;
第二个是showDom,用来展示校验完成后的(错误)提示;
第三个则是校验规则,你需要怎么去校验都可以写到里面,它是一个数组,数组成员是一个个的对象,对象包含strategy属性和errorMsg属性,分别是校验规则和错误提示。

例如下面就是我们规定的用户名需要进行的一些验证:

// isNonEmpty maxLength 这些是在 strategies 中已经定义好的规则
validator.add(userDom, showUser, [{strategy: 'isNonEmpty', errorMsg:'用户名不能为空'}, {strategy: 'maxLength:5', errorMsg: '用户名不能超过5'}]);
  • 需要注意的是这些传参的规则都是我们人为去规定的,例如为什么maxLength:5这样写呀?都是自己规定的,后面会通过字符串拆分得到这个数字。让别人看得懂,能用,就行。

让我们一鼓作气,把验证密码的规则都给添加上:

var validator = new Validator();

var ProxyRequest = (function () {

    // 添加校验规则
    validator.add(userDom, showUser, [{strategy: 'isNonEmpty', errorMsg:'用户名不能为空'}, 
    {strategy: 'maxLength:4', errorMsg: '用户名不能超过4'}]);
    
    validator.add(psDom, showPs, [{strategy: 'isNonEmpty', errorMsg:'密码不能为空'}, 
    {strategy: 'minLength:6', errorMsg: '密码长度不能小于6'}]);

    return function () {
       
        var flag = validator.start();  // 前面只是添加校验规则,这里才是真正开始验证,它返回是否验证成功

        var arg = arguments;
        if(flag) {
            myRequest.apply(this, arg);
        }
    }
}())

好了,上面一个Validator的使用介绍地差不多了,接下来我们来具体实现这个Validator。

Validator.prototype.strategies = {
    isNonEmpty: function () {
    },
    maxLength: function () {
    },
    minLength: function () {
    }
}
Validator.prototype.add = function (dom, showDom, strategyArr) {}
Validator.prototype.start = function () {}
function Validator() {
    // 缓存校验规则,start时一并校验
    this.cache = [];
   // 保存显示错误信息的DOM,方便清空再显示
    this.textDom = [];
}

前面说过,add方法给dom添加的校验规则必须存在于strategies里面,所以我们可以知道strategies里面有isNonEmpty、maxLength、minLength等具体校验方法,具体怎么校验呢,实现起来也很简单:

Validator.prototype.strategies = {
    // 判断传入的value是否为空,是则返回自定义的错误信息errorMsg,否则返回true表示验证成功
    isNonEmpty: function (value, errorMsg) {
        if(value == '') {
            return errorMsg;
        }
        return true;
    },
    // 除了传入value和errorMsg,还需要一个length来规定最大长度。下同。
    maxLength: function (value, length, errorMsg) {
        if(value.length > length) {
            return errorMsg;
        }
        return true;
    },
    minLength: function (value, length, errorMsg) {
        if(value != '' && value.length < length) {
            return errorMsg;
        }
        return true;
    }
}

然后我们再来实现一下add方法。首先它只是为了给dom添加校验规则,我们需要把这些规则缓存到cache里(也就是保存下来,不然你最后怎么知道校验谁呢),最后由start来触发校验。校验时,我们调用strategies里的方法进行校验,但是在这之前,我们必须先对传入的strategyArr进行一些处理,来看是使用哪条规则,该给这个规则入什么参数。

那么如何分离出规则和参数呢?具体我们看代码的实现:

Validator.prototype.add = function (dom, showDom, strategyArr) {
    // this == validator
    strategyArr.forEach(function (ele) {

        // 保存显示错误信息的DOM
        this.textDom.push(showDom);

        var self = this;

        //  将校验缓存到cache中,最后由start触发校验。
        this.cache.push(function () {
             
             // 先进行数据的处理,再调用strategies里的函数进行校验。

            // ele  --> {'strategy': 'isNonEmpty', errorMsg:'用户名不能为空'}, 
            // ele  --> {'strategy': 'maxLength:4', errorMsg: '用户名不能超过4'}

            // 分开规则和参数
            var arr = ele.strategy.split(':');

            // arr --> ['isNonEmpty']  
            //  arr --> ['maxLength', '4']

            // 取出规则
            var type = arr.shift();

            // arr --> [] ,  type  -> 'isNonEmpty'
            // arr --> ['4'] ,  type -> 'maxLength'

            // 再把input里的值传入arr的第一个位置,错误信息插入到最后一个位置,得到实参数组
            arr.unshift(dom.value);
            arr.push(ele.errorMsg);

            // 得到了对应的规则和实参数组,我们就可以通过apply调用strategies里的方法了
            var msg = self.strategies[type].apply(this, arr);

            // 如果返回值不是true,那么在showDom中显示错误信息
            if(msg !== true) {
                showDom.innerText = msg;
            }
            
            // 返回信息,供start函数判断是否全部校验都通过
            return msg;
        })
    }, this)  // 修改forEach回调里的this指向,使之能找到cache和textDom

通过一系列窒息操作,我们分离出了规则和实参数组。下面我解释一下这样操作的原因:
我们知道strategies里面的方法第一个参数和最后一个参数始终是value和errorMsg(人为规定),只是这两个值中间可能会有这个校验方法特有的参数。如果我们一个一个参数进行push的话,那么会需要一些额外的if-else判断,而通过shift unshift push这些骚操作,我们则不需要额外的判断,提高了效率。

接着我们再来实现start:

Validator.prototype.start = function () {

    var flag = true;

    // 先清空错误信息
    this.textDom.forEach(function (ele) {
        ele.innerText = '';
    });

    for(var i = 0; i < this.cache.length; i ++) {
        // 执行校验,并取得返回信息
        if(this.cache[i]() !== true) { 
            flag = false;  // 如果其中一个校验失败,那么flag就会变成false
        }
    }

    // 只有全部校验成功,最后flag才为true不变
    return flag;
}

到此,Validator基本封装完成,我们可以把这个类写到strategy.js里面,在需要验证表单的时候引入即可。

最后,我们再来说一下extend方法。如果我们想临时(不希望修改strategy.js)添加一个邮箱的验证,这是我们可以通过提供一个extend接口,然用户可以把自定义的规则添加到strategies里面,然后就可以通过add添加校验了。

添加到strategies需要传两个参数,一个是规则名,一个是处理函数。

Validator.extend({type: 'isEmail', handler: function (value, errorMsg) {
    if(value != '' && value.indexOf('@') == -1) {
        return errorMsg;
    }
    return true;
}})
validator.add(emDom, showEm, [{strategy: 'isNonEmpty', errorMsg:'邮箱不能为空'}, 
            {strategy: 'isEmail', errorMsg: '这不是邮箱可恶'}]);

把extend作为静态方法添加到Validator上。与添加到原型上的区别是,一来不需要通过过长的原型链找到extend,二是想扩展规则的时候不需要先new一个实例。

Validator.extend = function (config) {
    Validator.prototype.strategies[config.type] = config.handler;
}

到此,我们就大功告成了。以上就是我们运用策略模式的思想,来写的一个校验器。

小小总结

从上面可以看到,实现策略模式,最重要的就是对象上的strategies。他把一系列我们可能用到的算法封装起来,使得它易于切换,易于理解,易于拓展,符合开闭原则。再通过add、start这些方法,我们把算法的使用和算法的实现分离开来,提高了复用性,避免了早其他地方用到时许多重复的复制黏贴的工作。

@LyzSg LyzSg changed the title 前端必会设计模式之(三)策略模式 前端必会设计模式之(三) 策略模式 Jun 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant