Refactoring-design API extension mechanism

Refactoring-design API extension mechanism

1 Introduction

The last article mainly introduced some concepts of reconstruction and some simple examples. This time, let's talk about a refactoring scenario in the project in detail-design an extension mechanism for the API. The purpose is to facilitate the flexibility to respond to changes in demand in the future. Of course, whether you need to design for scalability depends on the requirements of the API. If you have any suggestions, please comment and leave a message.

2. Extensible manifestation

2-1.prototype

This can be said to be the most original extension in JS. For example, native JS does not provide an API to disrupt the order of arrays, but developers want to make it easy to use. In this case, they can only extend the prototype of the array. code show as below

//Array.prototype 
Array.prototype.upset=function(){
    return this.sort((n1,n2)=>Math.random() - 0.5);
}

let arr=[1,2,3,4,5];
//
arr.upset();
//
console.log(arr); 

operation result

The function is realized. But the above code, I just want to use an example to explain the extensibility, everyone just look at it. Don't imitate, and don't write like this in the project. This development is basically prohibited now. The reason is also very simple, as mentioned in the previous article. Repeat here.

This pollutes the native object Array, and the Array created by others will also be polluted, causing unnecessary overhead. The most frightening thing is that if the name you named is the same as the original method, the original method will be overwritten.

Array.prototype.push=function(){console.log(' ')}  
let arrTest=[123]
arrTest.push()
//result: 
//push w3c  

2-2.jQuery

Regarding the extensibility of jQuery, three APIs are provided: $.extend(), $.fn and $.fn.extend(). To extend jQuery itself, static methods, and prototype objects respectively. When writing plug-ins based on jQuery, the most inseparable thing should be $.fn.extend().

Reference link:

Understand jquery's $.extend(), $.fn and $.fn.extend()
Jquery custom plug-in $.extend(), $.fn and $.fn.extend()

2-3.VUE

To extend VUE, quote the official website ( plug-in ), there are generally the following ways to extend:

1. Add global methods or attributes, such as: vue-custom-element

2. Add global resources: instructions/filters/transitions, etc., such as vue-touch

3. Add some component options through the global mixin method, such as: vue-router

4. Add Vue instance methods by adding them to Vue.prototype.

5. A library that provides its own API and one or more of the functions mentioned above, such as vue-router

Based on VUE extension. In the component, the content of the plug-in provides an install method. as follows

Use components

The above several extensibility examples are native objects, libraries, and framework extensions. You may find it a bit exaggerated, so let's share an example that is commonly used in daily development.

3. Example-form validation

Looking at the extensibility examples above, let s look at the next example that is also used a lot in daily development: form validation. This piece can be said to be very simple, but doing it well is not easy to do universally. After reading "JavaScript Design Patterns and Development Practices", I used the strategy pattern to reconstruct the previous form validation function. Let's make a simple analysis.

The following content, the code will be too much, although the code is not difficult, it is strongly recommended that you don t just read it, you should read it, write it, and debug it. Otherwise, as a reader, you probably don t know what my code means, it s easy. stupid. The following code involves two pieces of knowledge: the open-closed principle and the strategy model. You can understand it yourself.

3-1. Original plan

/**
 * @description  
 * @param checkArr
 * @returns {boolean}
 */
function validateForm(checkArr){
    let _reg = null, ruleMsg, nullMsg, lenMsg;
    for (let i = 0, len = checkArr.length; i < len; i++) {
        //undefined 
        if (checkArr[i].el === undefined) {
            continue;
        }
        //
        ruleMsg = checkArr[i].msg || ' ';
        //
        nullMsg = checkArr[i].nullMsg || ' ';
        //
        lenMsg = checkArr[i].lenMsg || ' ' + checkArr[i].minLength + " " + checkArr[i].maxLength;
        //
        if (checkArr[i].noNull === true) {
            //
            if (checkArr[i].el === "" || checkArr[i].el === null) {
                return nullMsg;
            }
        }
        //
        if (checkArr[i].rule) {
            //
            switch (checkArr[i].rule) {
                case 'mobile':
                    _reg = /^1[3|4|5|7|8][0-9]\d{8}$/;
                    break;
                case 'tel':
                    _reg = /^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/;
                    break;
            }
            //
            if (!_reg.test(checkArr[i].el) && checkArr[i].el !== "" && checkArr[i].el !== null) {
                return ruleMsg;
            }
        }
        //
        if (checkArr[i].el !== null && checkArr[i].el !== '' && (checkArr[i].minLength || checkArr[i].maxLength)) {
            if (checkArr[i].el.toString().length < checkArr[i].minLength || checkArr[i].el.toString().length > checkArr[i].maxLength) {
                return lenMsg;
            }
        }
    }
    return false;
} 

Function call method

    let testData={
        phone:'18819323632',
        pwd:'112'
    }

    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: ' ',rule: "mobile", msg: ' '},
        {el: testData.pwd, noNull: true, nullMsg: ' ',lenMsg:' ',minLength:6,maxLength:18}
    ]);
    //
    if (_tips) {
        alrt(_tips);
    } 

3-2. There is a problem

This method is uncomfortable for everyone, because there are indeed more problems.

1. To enter a field, there may be three kinds of judgments (null value, rule, length). If it is just a simple phone number rule check, it will go through two other unnecessary checks, causing unnecessary overhead. The running process is as follows.


2. In the rule check, there are only these types of check. If you want to add other checks, such as adding a date rule, it cannot be completed. If you keep modifying the source code, it may result in huge functions.

3. The writing is not elegant, and the call is inconvenient.

3-3. Alternative plan

In view of the three problems in 2-2 above, make improvements one by one.

Because the calling method is inconvenient, it is difficult to optimize and reconstruct the internal code without changing the validateForm calling method, and increase the scalability. It is impossible to rewrite this method, because there are some places where this API is already used, and it is not practical to change one by one, so instead of modifying the validateForm, create a new API: validate. In future projects, we will try our best to guide colleagues to abandon validateForm and use the new API.

The first one above is to optimize the verification rules. Each verification (such as null value, length, rule) is a simple verification, and no other unnecessary verifications are performed. The running process is as follows.


let validate = function (arr) {
    let ruleData = {
        /**
         * @description  
         * @param val
         * @param msg
         * @return {*}
         */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param msg
         * @return {*}
         */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    let ruleMsg, checkRule, _rule;
    for (let i = 0, len = arr.length; i < len; i++) {
        //
        if (arr[i].el === undefined) {
            return ' '
        }
        //
        for (let j = 0; j < arr[i].rules.length; j++) {
            //
            checkRule = arr[i].rules[j].rule.split(":");
            _rule = checkRule.shift();
            checkRule.unshift(arr[i].el);
            checkRule.push(arr[i].rules[j].msg);
            //
            ruleMsg = ruleData[_rule].apply(null, checkRule);
            if (ruleMsg) {
                //
                return ruleMsg;
            }
        }
    }
};
let testData = {
    name: '',
    phone: '18819522663',
    pw: 'asda'
}
//
console.log(validate([
    {
        //
        el: testData.phone,
        //
        rules: [
            {rule: 'isNoNull', msg: ' '}, {rule: 'isMobile', msg: ' '}
        ]
    },
    {
        el: testData.pw,
        rules: [
            {rule: 'isNoNull', msg: ' '},
            {rule:'minLength:6',msg:' 6'}
        ]
    }
])); 

The above is the completion of the first step of optimization. Before proceeding to the second step, everyone would like to think that if the ruleData above is not enough, for example, if I want to add a date range verification, I must modify the ruleData and add an attribute. as follows

let ruleData = {
    //
    /**
     * @description  
     * @param val
     * @param msg
     * @return {*}
     */
    isDateRank(val,msg) {
        let _date=val.split(',');
        if(new Date(_date[0]).getTime()>=new Date(_date[1]).getTime()){
            return msg;
        }
    }
} 

If there are other rules, you have to change this again, which violates the open-closed principle. If multiple people share this function, there may be many rules, and ruleData will become huge, causing unnecessary overhead. For example, the A page has a check of the amount, but only the A page has it. If you change it according to the above method, the verification rule of the amount will be loaded on page B, but it will not be used at all, resulting in a waste of resources.

So the open-closed principle is applied below. Add scalability to the verification rules of the function. Before the actual operation, everyone should be confused, because a function can perform verification operations, and there are operations to add verification rules. A function that does two things violates the single principle. It will be difficult to maintain at that time, so the recommended approach is to do it separately. Write as follows.

let validate = (function () {
    let ruleData = {
        /**
         * @description  
         * @param val
         * @param msg
         * @return {*}
         */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param msg
         * @return {*}
         */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        /**
         * @description  
         * @param arr
         * @return {*}
         */
        check: function (arr) {
            let ruleMsg, checkRule, _rule;
            for (let i = 0, len = arr.length; i < len; i++) {
                //
                if (arr[i].el === undefined) {
                    return ' '
                }
                //
                for (let j = 0; j < arr[i].rules.length; j++) {
                    //
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    //
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        //
                        return ruleMsg;
                    }
                }
            }
        },
        /**
         * @description  
         * @param type
         * @param fn
         */
        addRule:function (type,fn) {
            ruleData[type]=fn;
        }
    }
})();
//- 
console.log(validate.check([
    {
        //
        el: testData.mobile,
        //
        rules: [
            {rule: 'isNoNull', msg: ' '}, {rule: 'isMobile', msg: ' '}
        ]
    },
    {
        el: testData.password,
        rules: [
            {rule: 'isNoNull', msg: ' '},
            {rule:'minLength:6',msg:' 6'}
        ]
    }
]));
//- 
validate.addRule('isDateRank',function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        return msg;
    }
});
//- 
console.log(validate.check([
    {
        el:['2017-8-9 22:00:00','2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:' '
        }]
    }
    
])); 

As shown in the code above, here you need to add a date range check to ruleData, which can be added here. But things that cannot access and modify ruleData have a protective effect. Another is that, for example, adding a date check on page A only exists on page A and will not affect other pages. If date verification can be used in other places, you can consider adding a date verification rule to ruleData in the global.

As for the third question, this idea may not be too elegant, and the call is not too convenient, but as far as I can think of, this is the best solution.

This seems to have been done, but you may feel that there is a situation that has not been able to deal with, such as the following, which cannot be done.

Because of the check interface above, as long as there is an error, it will jump out immediately and will not check the next one. If you want to implement the following functions, you must implement it. If there is a value check error, record the error message and continue to check the next one. After all the checks have been performed, as shown in the following flowchart.


After the execution is completed, and then the results are returned together, then an interface must be exposed below.

The code is as follows (we ignore the attribute of alias first)

let validate= (function () {
    let ruleData = {
        /**
         * @description  
         * @param val
         * @param msg
         * @return {*}
         */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param length
         * @param msg
         * @return {*}
         */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /**
         * @description  
         * @param val
         * @param msg
         * @return {*}
         */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        check: function (arr) {
            //
        },
        addRule:function (type,fn) {
            //
        },
        /**
         * @description  
         * @param arr
         * @return {*}
         */
        checkAll: function (arr) {
            let ruleMsg, checkRule, _rule,msgArr=[];
            for (let i = 0, len = arr.length; i < len; i++) {
                //
                if (arr[i].el === undefined) {
                    return ' '
                }
                //

                //
                for (let j = 0; j < arr[i].rules.length; j++) {
                    //
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    //
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        //
                        msgArr.push({
                            el:arr[i].el,
                            alias:arr[i].alias,
                            rules:_rule,
                            msg:ruleMsg
                        });
                    }
                }
            }
            //
            return msgArr.length>0?msgArr:false;
        }
    }
})();
let testData = {
    name: '',
    phone: '188',
    pw: 'asda'
}
//- 
validate.addRule('isDateRank',function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        return msg;
    }
});
//
console.log(validate.checkAll([
    {
        //
        el: testData.phone,
        alias:'mobile',
        //
        rules: [
            {rule: 'isNoNull', msg: ' '}, {rule: 'isMobile', msg: ' '},{rule:'minLength:6',msg: ' 6'}
        ]
    },
    {
        el: testData.pw,
        alias:'pwd',
        rules: [
            {rule: 'isNoNull', msg: ' '},
            {rule:'minLength:6',msg:' 6'}
        ]
    },
    {
        el:['2017-8-9 22:00:00','2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:' '
        }]
    }
])); 

Seeing the result, all the records of illegal data are now returned. As for the alias at the time, its usefulness is now revealed.
For example, the page is rendered by vue, which can be handled like this according to alias.



If it is rendered by jQuery, it can be handled like this according to alias.



3-4. Backward compatibility solution

Because the project used the previous verification API before, it cannot be used across the board, and the use cannot be affected before the previous API is abandoned. So we need to rewrite the previous validateForm to make it compatible with the new API: validate.

    let validateForm=function (arr) {
        let _param=[],_single={};
        for(let i=0;i<arr.length;i++){
            _single={};
            _single.el=arr[i].el;
            _single.rules=[];
            //
            if(arr[i].noNull){
                _single.rules.push({
                    rule: 'isNoNull',
                    msg: arr[i].nullMsg||' '
                })
            }
            //
            if(arr[i].minLength){
                _single.rules.push({
                    rule: 'minLength:'+arr[i].minLength,
                    msg: arr[i].lenMsg ||' '
                })
            }
            //
            if(arr[i].maxLength){
                _single.rules.push({
                    rule: 'maxLength:'+arr[i].maxLength,
                    msg: arr[i].lenMsg ||' '
                })
            }
            //
            //
            let _ruleData={
                mobile:'isMobile'
            }
            if(arr[i].rule){
                _single.rules.push({
                    rule: _ruleData[arr[i].rule],
                    msg: arr[i].msg ||' '
                })
            }
            _param.push(_single);
        }
        let _result=validate.check(_param);
        return _result?_result:false;
    }
    let testData={
        phone:'18819323632',
        pwd:'112'
    }
    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: ' ',rule: "mobile", msg: ' '},
        {el: testData.pwd, noNull: true, nullMsg: ' ',lenMsg:' ',minLength:6,maxLength:18}
    ]);
    console.log(_tips) 

4. Summary

That's it for today's example. This example is nothing more than adding scalability to the API. This example is relatively simple, not difficult. It's easy to understand if you run this code on the browser. If you have any better suggestions for this example, or if you have any problems with the code, please leave a message in the comment area, so that you can communicate and learn from each other.



-------------------------Gorgeous dividing line--------------------

Want to know more, pay attention to my WeChat public account: Shouhou Book Pavilion