资讯专栏INFORMATION COLUMN

重构 - 设计API的扩展机制

Lowky / 3029人阅读

1.前言

上篇文章,主要介绍了重构的一些概念和一些简单的实例。这一次,详细的说下项目中的一个重构场景--给API设计扩展机制。目的就是为了方便以后能灵活应对需求的改变。当然了,是否需要设计扩展性这个要看API的需求。如果大家有什么建议,欢迎评论留言。

2.扩展性表现形式 2-1.prototype

这个可以说是JS里面最原的一个扩展。比如原生JS没有提供打乱数组顺序的API,但是开发者又想方便使用,这样的话,就只能扩展数组的prototype。代码如下

//扩展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);

运行结果

功能是实现了。但是上面的代码,只想借用例子讲解扩展性,大家看下就好。不要模仿,也不要在项目这样写。现在基本都禁止这样开发了。理由也很简单,之前的文章也有提到过。这里重复一下。

这样就污染了原生对象Array,别人创建的Array也会被污染,造成不必要的开销。最可怕的是,万一自己命名的跟原生的方法重名了,就被覆盖原来的方法了。

Array.prototype.push=function(){console.log("守候")}  
let arrTest=[123]
arrTest.push()
//result:守候
//push方法有什么作用,大家应该知道,不知道的可以去w3c看下

2-2.jQuery

关于 jQuery 的扩展性,分别提供了三个API:$.extend()、$.fn和$.fn.extend()。分别对jQuery的本身,静态方法,原型对象进行扩展,基于jQuery写插件的时候,最离不开的应该就是$.fn.extend()。

参考链接:

理解jquery的$.extend()、$.fn和$.fn.extend()
Jquery自定义插件之$.extend()、$.fn和$.fn.extend()

2-3.VUE

对VUE进行扩展,引用官网(插件)的说法,扩展的方式一般有以下几种:

1.添加全局方法或者属性,如: vue-custom-element

2.添加全局资源:指令/过滤器/过渡等,如 vue-touch

3.通过全局 mixin 方法添加一些组件选项,如: vue-router

4.添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。

5.一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如 vue-router

基于VUE的扩展。在组件,插件的内容提供一个install方法。如下

使用组件

上面几个扩展性的实例分别是原生对象,库,框架的扩展,大家可能觉得有点夸夸而谈,那下面就分享一个日常开发常用的一个实例。
3.实例-表单验证

看了上面那些扩展性的实例,下面看下一个在日常开发使用得也很多的一个实例:表单验证。这块可以说很简单,但是做好,做通用不简单。看了《JavaScript设计模式与开发实践》,用策略模式对以前的表单验证函数进行了一个重构。下面进行一个简单的分析。

下面的内容,代码会偏多,虽然代码不难,但还是强烈建议大家不要只看,要边看,边写,边调试,不然作为读者,很可能不知道我的代码是什么意思,很容易懵。下面的代码回涉两个知识:开放-封闭原则和策略模式,大家可以自行了解。
3-1.原来方案
/**
 * @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;
}

函数调用方式

    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) {
        alert(_tips);
    }
3-2.存在问题

这样方法,相信大家看的也难受,因为问题确实是比较多。

1.一个字段进入,可能要经过三种判断(空值,规则,长度)。如果只是一个简单的电话号码规则校验,就要经过其他两种没必要的校验,造成不必要的开销。运行的流程就如同下面。

2.规则校验里面,只有这几种校验,如果要增加其他校验,比如增加一个日期的规则,无法完成。如果一直修改源码,可能会导致函数巨大。

3.写法不优雅,调用也不方便。

3-3.代替方案

针对上面2-2的三个问题,逐个进行改善。

因为调用方式就不方便,很难在不改变validateForm调用方式的同时,优化重构内部的代码,又增加扩展性。重写这个方法又不可能,因为有个别的地方已经使用了这个API,自己一个一个的改不现实,所以就不修改这个validateForm,新建一个新的API:validate。在以后的项目上,也尽量引导同事放弃validateForm,使用新的API。

上面第一个,优化校验规则,每次校验(比如空值,长度,规则),都是一个简单的校验,不再执行其他没必要的校验。运行流程如同下面。

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"}
        ]
    }
]));

如果又有其它的规则,又得改这个,这样就违反了开放-封闭原则。如果多人共用这个函数,规则可能会很多,ruleData会变的巨大,造成不必要的开销。比如A页面有金额的校验,但是只有A页面有。如果按照上面的方式改,在B页面也会加载金额的校验规则,但是根本不会用上,造成资源浪费。

所以下面应用开放-封闭原则。给函数的校验规则增加扩展性。在实操之前,大家应该会懵,因为一个函数,可以进行校验的操作,又有增加校验规则的操作。一个函数做两件事,就违反了单一原则。到时候也难维护,所以推荐的做法就是分接口做。如下写法。

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:"日期范围不正确"
        }]
    }
    
]));

如上代码所示,这里需要往ruleData添加日期范围的校验,这里可以添加。但是不能访问和修改ruleData的东西,有一个保护的作用。还有一个就是,比如在A页面添加日期的校验,只在A页面存在,不会影响其它页面。如果日期的校验在其它地方都可能用上,就可以考虑,在全局里面为ruleData添加日期的校验的规则。

至于第三个问题,这样的想法,可能不算太优雅,调用也不是太方便,但是就我现在能想到的,这个就是最好方案啊了。

这个看似是已经做完了,但是大家可能觉得有一种情况没能应对,比如下面这种,做不到。

因为上面的check接口,只要有一个错误了,就立马跳出了,不会校验下一个。如果要实现下面的功能,就得实现,如果有一个值校验错误,就记录错误信息,继续校验下一个,等到所有的校验都执行完了之后,如下面的流程图。

代码上面(大家先忽略alias这个属性)

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:"日期范围不正确"
        }]
    }
]));

看到结果,现在所有的不合法的数据的记录都返回回来了。至于当时alias现在揭晓用处。
比如页面是vue渲染的,根据alias可以这样处理。

如果是jQuery渲染的,根据alias可以这样处理。

3-4.向下兼容方案

因为项目之前有使用了以前的校验API,不能一道切,在以前的API没废弃之前,不能影响之前的使用。所以要重写以前的validateForm,使之兼容现在的新API:validate。

    let validateForm=function (arr) {
        let _param=[],_single={};
        for(let i=0;i
4.小结

今天的例子就到这里了,这个例子,无非就是给API增加扩展性。这个例子比较简单,不算难。大家用这个代码在浏览器上运行,就很好理解。如果大家对这个例子有什么更好的建议,或者代码上有什么问题,欢迎在评论区留言,大家多交流,相互学习。

-------------------------华丽的分割线--------------------

想了解更多,关注关注我的微信公众号:守候书阁

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94783.html

相关文章

  • 容器和应用程序:扩展重构或重建?

    摘要:综上所述,为使传统应用程序容器化,有以下几种路径扩展重构或者重建。在中运行应用程序的最大障碍之一是临时文件系统。大体来说,利用容器技术实现传统应用程序的现代化并没有硬性规则。 技术领域是不断变化的,因此,任何应用程序都可能在很短时间内面临过时甚至淘汰,更新换代的速度之快给人的感觉越来越强烈,我们如何使传统应用程序保持活力不落伍?工程师想的可能是从头开始重建传统应用程序,这与公司的业务目...

    tigerZH 评论0 收藏0
  • 微服务实战:从架构到发布(一)

    摘要:微服务集成服务间通信微服务架构下,应用的服务直接相互独立。微服务架构倾向于降低中心消息总线类似于的依赖,将业务逻辑分布在每个具体的服务终端。 引言:微服务是当前软件架构领域非常热门的词汇,能找到很多关于微服务的定义、准则,以及如何从微服务中获益的文章,在企业的实践中去应用微服务的资源却很少。本篇文章中,会介绍微服务架构(Microservices Architecture)的基础概念,...

    libin19890520 评论0 收藏0
  • 微服务实战:从架构到发布(一)

    摘要:微服务集成服务间通信微服务架构下,应用的服务直接相互独立。微服务架构倾向于降低中心消息总线类似于的依赖,将业务逻辑分布在每个具体的服务终端。 引言:微服务是当前软件架构领域非常热门的词汇,能找到很多关于微服务的定义、准则,以及如何从微服务中获益的文章,在企业的实践中去应用微服务的资源却很少。本篇文章中,会介绍微服务架构(Microservices Architecture)的基础概念,...

    HtmlCssJs 评论0 收藏0
  • 一文读懂微服务架构重构策略

    摘要:相反,它由单体中的适配器和使用一个或多个进程间通信机制的服务组成。因为微服务架构的本质是一组围绕业务功能组织的松耦合服务。如果你尝试将此类功能实现为服务,则通常会发现,由于过多的进程间通信而导致性能下降。这是快速展示微服务架构价值的好方法。你很有可能正在处理大型复杂的单体应用程序,每天开发和部署应用程序的经历都很缓慢而且很痛苦。微服务看起来非常适合你的应用程序,但它也更像是一项遥不可及的必杀...

    jaysun 评论0 收藏0
  • KubernetesDevice Plugin设计解读

    摘要:摘要的生态地位已经确立,可扩展性将是其发力的主战场。该功能由于只是替代了做了些更名的工作,所以在已经是稳定的状态了。异构计算作为非常重要的新战场,非常重视。而异构计算需要强大的计算力和高性能网络,需要提供一种统一的方式与等高性能硬件集成。 摘要: Kubernetes的生态地位已经确立,可扩展性将是其发力的主战场。异构计算作为非常重要的新战场,Kubernetes非常重视。而异构计算需...

    bladefury 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<