1. 程式人生 > 實用技巧 >[JS設計模式]:策略模式及應用-獎金、表單驗證的實現(5)

[JS設計模式]:策略模式及應用-獎金、表單驗證的實現(5)

介紹

策略模式的意義是定義一系列的演算法,把它們一個個封裝起來,並且使它們可相互替換。此模式讓演算法的變化不會影響到使用演算法的客戶。

實現

舉一個例子,比如我們做資料合法性校驗,一般是通過swich來實現,或者通過if語句來實現,如果校驗規則多了的話,那麼程式碼的擴充套件性和維護性就很差了,而且進行單元測試就越來越複雜,程式碼如下:

var validator = {
    validate: function(value,type) {
        switch(type) {
            case 'isNonEmpty':
                return
true case 'isNumber': return true; case 'isAlphaNum': return true; default: return true; } } } alert(validator.validate('123','isNonEmpty'))

怎麼避免上面程式碼的弊端呢,我們可以使用策略模式把相同的工作程式碼封裝成不同的驗證類,我們只需要通過傳遞不同的名稱來呼叫不同的驗證類方法(也即是不同的演算法),實現程式碼如下:

var validator = {
    types: { // 存放驗證規則
        isNonEmpty: {
            validate: function(value) {
                return value !== ''
            },
            instructions: '傳入的值不能為空'
        },
        isNumber: {
            validate: function(value) {
                return !isNaN(value);
            },
            instructions: 
'傳入的值不是數字' }, isAlphaNum: { validate: function(value) { return !/[^a-z0-9]/i.test(value) }, instructions: '傳入的值只能是數字或者字母,不能是特殊字元' } }, config: {}, // 需要驗證的型別 messages: [], // 存放錯誤資訊 validate: function(data) { // 傳入的data 為key-value的鍵值對 var i, type, checker, resultOk; this.messages = []; // 首先清空錯誤資訊 for(i in data) { if(data.hasOwnProperty(i)) { // 判斷i不是原型上的屬性 type = this.config[i]; // 獲取驗證型別 if(!type) { // 沒有當前校驗型別直接跳過(不需要驗證的) continue; } checker = this.types[type]; // 獲取驗證規則的驗證類方法 if(!checker) { // 驗證規則類不存在 直接丟擲異常 throw { name: "ValidationError", message: "No handler to validate type " + type } } resultOk = checker.validate(data[i]); if(!resultOk) { // 驗證不通過 this.messages.push(checker.instructions); } } } return this.hasErrors(); }, hasErrors: function() { return this.messages.length !== 0; } }

使用方式如下:

var data = {
    firstName: '',
    lasName: 'shu',
    age: '',
    userName: 'tom shu'
}
validator.config = {
    firstName: 'isNonEmpty',
    age: 'isNumber',
    userName: 'isAlphaNum'
}

validator.validate(data);
if(validator.hasErrors()) {
    console.log(validator.messages.join("\n"));
}
// 結果:
// 傳入的值不能為空
// 傳入的值只能是數字或者字母,不能是特殊字元

其它策略模式示例

jquery中使用的animate方法

$( div ).animate( {"left: 200px"}, 1000, 'linear' );  //勻速運動
$( div ).animate( {"left: 200px"}, 1000, 'cubic' );  //三次方的緩動

這 2 句程式碼都是讓 div 在 1000ms 內往右移動 200 個畫素. linear(勻速) 和 cubic(三次方緩動) 就是一種策略模式的封裝.

計算獎金

比如公司的年終獎是根據員工的工資和績效來考核的,績效為A的人,年終獎為工資的4倍,績效為B的人,年終獎為工資的3倍,績效為C的人,年終獎為工資的2倍;現在我們使用一般的編碼方式會如下這樣編寫程式碼:

var calculateBouns = function(salary,level) {
    if(level === 'A') {
        return salary * 4;
    }
    if(level === 'B') {
        return salary * 3;
    }
    if(level === 'C') {
        return salary * 2;
    }
};
// 呼叫如下:
console.log(calculateBouns(4000,'A')); // 16000
console.log(calculateBouns(2500,'B')); // 7500

缺點:函式含有很多if語句,缺乏彈性,演算法複用性差,如果其它地方有類似的演算法,但是規則不一樣,這些程式碼不能通用。

使用策略模式程式碼如下:

//程式碼如下:
var obj = {
        "A": function(salary) {
            return salary * 4;
        },
        "B" : function(salary) {
            return salary * 3;
        },
        "C" : function(salary) {
            return salary * 2;
        } 
};
var calculateBouns =function(level,salary) {
    return obj[level](salary);
};
console.log(calculateBouns('A',10000)); // 40000

策略模式不僅僅只封裝演算法,我們還可以對用來封裝一系列的業務規則,只要這些業務規則目標一致,我們就可以使用策略模式來封裝它們

表單校驗

比如常見的就是註冊頁面,需要對使用者名稱,密碼,手機號等進行規則校驗,驗證規則如下:

  • 使用者名稱不能為空;
  • 密碼不能小於6位;
  • 手機號碼符合手機正則規則

HTML程式碼如下:

<form action="" id="registerForm" method="post" onsubmit="return submitValidate()">
    <p>
        <label>請輸入使用者名稱:</label>
        <input type="text" name="userName" />
    </p>
    <p>
        <label>請輸入密碼:</label>
        <input type="text" name="password" />
    </p>
    <p>
        <label>請輸入手機號碼:</label>
        <input type="text" name="phoneNumber" />
    </p>
    <div>
        <button type="submit">提交</button>
    </div>
</form>

submitValidate驗證方法如下:

function submitValidate() {
    var registerForm = document.getElementById("registerForm");
    if(registerForm.userName.value === '') {
            alert('使用者名稱不能為空');
            return false;
        }
    else if(registerForm.password.value.length < 6) {
        alert("密碼的長度不能小於6位");
        return false;
    }
    else if(!/(^1[0-9]{10}$)/.test(registerForm.phoneNumber.value)) {
        alert("手機號碼格式不正確");
        return false;
    }
    return true;
}

缺點:

  • submitValidate函式中的if else-if程式碼會根據驗證項而逐漸變大;
  • submitValidate函式缺乏彈性,如果新增新的校驗規則是新增else-if語句,但是如果是修改原來的驗證規則,那麼就需要改函式內的程式碼,違反開放-封閉原則;
  • 演算法的複用性差,如果其它頁面也需要用類似的校驗,那麼這個方法就不能共用了,可以又是複製程式碼。

下面我們使用策略模式來重構上面的程式碼。

第一步,封裝策略物件,也即是驗證的不同演算法,程式碼如下:

var strategys = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小長度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手機號格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[0-9]{10}$)/.test(value)) {
            return errorMsg;
        }
    }
}

第二步,實現Validator類,Validator類在這裡作為Context,負責接收使用者的請求並委託給strategy物件。

通俗的話就是新增表單中需要驗證的一些規則以及獲取驗證結果(是否驗證通過),如下程式碼:

function Validator() {
    this.cache = []; // 儲存效驗規則
}

Validator.prototype = {
    constructor: Validator,
    add: function(dom,rule,errorMsg) {
        var str = rule.split(":"); // minLength:6的場景
        var fn = function() {
            var strategyType = str.shift(); // 刪除str陣列中的第一個元素並返回,即是獲取校驗規則的函式名
            str.unshift(dom.value); // 往陣列str的第一位插入value值
            str.push(errorMsg);
            return strategys[strategyType].apply(dom,str);
        }
        this.cache.push(fn);
    },
    start: function() {
        for(var i = 0, fn; fn = this.cache[i++];) {
            var msg = fn();
            if(msg) {
                return msg;
            }
        }
    }
} 

呼叫方式:

function submitValidate() {
    var registerForm = document.getElementById("registerForm");
    var validator = new Validator();
    validator.add(registerForm.userName,'isNotEmpty', '使用者名稱不能為空');
    validator.add(registerForm.password,'minLength:6', '密碼的長度不能小於6位');
    validator.add(registerForm.phoneNumber,'mobileFormat', '手機號碼格式不正確');
    var resultMsg = validator.start();
    if(resultMsg) {
        alert(resultMsg);
        return false;
    }
    return true;
}

完整的JS程式碼:

var strategys = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小長度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手機號格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[0-9]{10}$)/.test(value)) {
            return errorMsg;
        }
    }
}

function Validator() {
    this.cache = []; // 儲存效驗規則
}

Validator.prototype = {
    constructor: Validator,
    add: function(dom,rule,errorMsg) {
        var str = rule.split(":"); // minLength:6的場景
        var fn = function() {
            var strategyType = str.shift(); // 刪除str陣列中的第一個元素並返回,即是獲取校驗規則的函式名
            str.unshift(dom.value); // 往陣列str的第一位插入value值
            str.push(errorMsg);
            return strategys[strategyType].apply(dom,str);
        }
        this.cache.push(fn);
    },
    start: function() {
        for(var i = 0, fn; fn = this.cache[i++];) {
            var msg = fn();
            if(msg) {
                return msg;
            }
        }
    }
} 

function submitValidate() {
    var registerForm = document.getElementById("registerForm");
    var validator = new Validator();
    validator.add(registerForm.userName,'isNotEmpty', '使用者名稱不能為空');
    validator.add(registerForm.password,'minLength:6', '密碼的長度不能小於6位');
    validator.add(registerForm.phoneNumber,'mobileFormat', '手機號碼格式不正確');
    var resultMsg = validator.start();
    if(resultMsg) {
        alert(resultMsg);
        return false;
    }
    return true;
}

以上程式碼我們只實現了給一個dom元素繫結一條驗證規則,那如果需要繫結多條驗證規則呢?

比如上面的程式碼我們只能效驗輸入框是否為空,validator.add(registerForm.userName,'isNotEmpty','使用者名稱不能為空');但是如果我們既要效驗輸入框是否為空,還要效驗輸入框的長度不要小於10位的話,那麼我們期望需要像如下傳遞引數:

validator.add(registerForm.userName,[{strategy:'isNotEmpty',errorMsg:'使用者名稱不能為空'},{strategy: 'minLength:10',errorMsg:'使用者名稱長度不能小於10位'}])

我們只需要修改一下add方法即可,如下程式碼:

function Validator() {
    this.cache = []; // 儲存效驗規則
}

Validator.prototype = {
    constructor: Validator,
    add: function(dom,rules) {
        var self = this;
        for(var i = 0, len = rules.length; i < len; i++) {
            var rule = rules[i];
            (function(rule){
                var str = rule.strategy.split(":"); // minLength:6的場景
                var fn = function() {
                    var strategyType = str.shift(); // 刪除str陣列中的第一個元素並返回,即是獲取校驗規則的函式名
                    str.unshift(dom.value); // 往陣列str的第一位插入value值
                    str.push(rule.errorMsg);
                    return strategys[strategyType].apply(dom,str);
                }
                self.cache.push(fn);
            })(rule)
        }
    },
    start: function() {
        for(var i = 0, fn; fn = this.cache[i++];) {
            var msg = fn();
            if(msg) {
                return msg;
            }
        }
    }
} 

呼叫方式改變一下:

function submitValidate() {
    var registerForm = document.getElementById("registerForm");
    var validator = new Validator();
    validator.add(registerForm.userName, [{
        strategy: 'isNotEmpty',
        errorMsg: '使用者名稱不能為空'
    }, {
        strategy: 'minLength:10',
        errorMsg: '使用者名稱長度不能小於10位'
    }]);
    validator.add(registerForm.password, [{
        strategy: 'minLength:6',
        errorMsg: '密碼的長度不能小於6位'
    }]);
    validator.add(registerForm.phoneNumber, [{
        strategy: 'mobileFormat',
        errorMsg: '手機號碼格式不正確'
    }]);
    var resultMsg = validator.start();
    if (resultMsg) {
        alert(resultMsg);
        return false;
    }
    return true;
}

完整的程式碼如下:

var strategys = {
    isNotEmpty: function(value, errorMsg) {
        if (value === '') {
            return errorMsg;
        }
    },
    // 限制最小長度
    minLength: function(value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg;
        }
    },
    // 手機號格式
    mobileFormat: function(value, errorMsg) {
        if (!/(^1[0-9]{10}$)/.test(value)) {
            return errorMsg;
        }
    }
}

function Validator() {
    this.cache = []; // 儲存效驗規則
}

Validator.prototype = {
    constructor: Validator,
    add: function(dom, rules) {
        var self = this;
        for (var i = 0, len = rules.length; i < len; i++) {
            var rule = rules[i];
            (function(rule) {
                var str = rule.strategy.split(":"); // minLength:6的場景
                var fn = function() {
                    var strategyType = str.shift(); // 刪除str陣列中的第一個元素並返回,即是獲取校驗規則的函式名
                    str.unshift(dom.value); // 往陣列str的第一位插入value值
                    str.push(rule.errorMsg);
                    return strategys[strategyType].apply(dom, str);
                }
                self.cache.push(fn);
            })(rule)
        }
    },
    start: function() {
        for (var i = 0, fn; fn = this.cache[i++];) {
            var msg = fn();
            if (msg) {
                return msg;
            }
        }
    }
}

function submitValidate() {
    var registerForm = document.getElementById("registerForm");
    var validator = new Validator();
    validator.add(registerForm.userName, [{
        strategy: 'isNotEmpty',
        errorMsg: '使用者名稱不能為空'
    }, {
        strategy: 'minLength:10',
        errorMsg: '使用者名稱長度不能小於10位'
    }]);
    validator.add(registerForm.password, [{
        strategy: 'minLength:6',
        errorMsg: '密碼的長度不能小於6位'
    }]);
    validator.add(registerForm.phoneNumber, [{
        strategy: 'mobileFormat',
        errorMsg: '手機號碼格式不正確'
    }]);
    var resultMsg = validator.start();
    if (resultMsg) {
        alert(resultMsg);
        return false;
    }
    return true;
}

當然我們也可以把驗證各種型別的演算法放到建構函式Validator原型上,這兒就不處理了。

總結

策略模式優點:

  • 策略模式利用組合,委託等技術和思想,有效的避免很多if條件語句。
  • 策略模式提供了開放-封閉原則,使程式碼更容易理解和擴充套件。
  • 策略模式中的程式碼可以複用。

參考