代码重构 - 前端部分代码

缘起

由于工作的变动,转岗到了公司的另外一个项目里,目前的主要工作在编码方面,负责将一个原来标准的J2EE(Spring, SpringMVC,MyBatis)项目,重构成基于Restful的前后端分离的项目,后端采用Spring Boot,前端部分则采用Vue。

这里计划用两篇博客记录一下重构中的一些点,一篇为前端部分,一篇为后端部分,这篇为前端部分。
由于处在不同的职位上,所关心的内容是不同的,比如产品经理更加关心产品的功能、业务的完成度、项目经理更加关注项目的进度等,另外一方面,从代码层面来说,还是比较主观的,不同的开发人员,写出来的代码也会差别很大,所以这里仅是个人的重构记录。下面就闲话少说,“talk is cheap, show me the code”。

业务背景

这里首先交代下重构部分的业务,如下图所以,这是个比较典型的数据表格,展示了业务数据,以及相关的操作,查看详情,编辑,删除,并且可以多选,并且进行对多条数据进行批量的操作。


需求原型

下图是完成了的数据表格部分,隐藏了其中涉及到的业务数据

数据表格

其主要的功能为:

  1. 表格右侧的“操作”部分,主要是针对单条记录的操作,如查看,修改,删除
  2. 表格下方的批量操作部分,当选中多条记录之后,执行不同的批量操作,如批量发送邀请,批量创建账号,批量删除等。针对不同的操作,在执行相应的操作时候,需要进行不同的前端验证,如开通邀请,则需要验证所选择的记录的电话号码不能为空,邮箱不能为空,销帮帮ID是否存在,等。

代码实现

简单介绍完需求之后,逻辑并不复杂,从代码实现角度,主要就是如下几步:
1、响应按钮点击
2、在响应的方法中遍历选中的数据
3、对数据进行校验,如何校验失败,则进行相应的提示
4、将数据提交到后端

实现起来也是相当的明了。

原来的实现

这部分主要描述下上述需求的原来的实现部分,这里不会贴出全部的代码,主要还是将意图表达出来。另外,这个项目的前端展示部分是基于JSP+jQuery+Vue的,所以原来页面部分逻辑较为复杂,一部分数据是传统的基于表单的,一部分数据是jQuery Ajax方式的,一部分数据则是Vue(axios)方式。

下面的三张截图,第一张是按钮部分,针对不同的功能,对应不同的响应方法:


image.png

下面两张是其中两个方法的具体实现:


方法一
方法二

由于逻辑不复杂,所以实现上也是比较的明了。基本上完成过程式的代码实现,遍历选择的数据,对数据进行校验,然后调用后台对应的接口。

过程式的代码,优点是代码清晰明了,基本上完全反映实现的意图。缺点也比较的明显,大量的重复代码,拿贴出来的两段代码来看,基本上都是相同的,复制粘贴一个方法,然后稍微的改一改。这里不去写这些的弊端,重点还是放在重构部分的内容上。

重构过程

首先上面的代码,从功能角度来说,是可以工作的代码,但是,很多的重复代码。实际上很容易就可以发现其中可以重用的部分,如公司名称的校验,销帮帮ID的校验,邮箱的校验等。
那么,最简单的实现重用的方式,可以将其中的检验部分抽出来,形成一个一个单独的验证方法,如:

validateEmailXXX()
validateCompanyNameXXX()

这样在不同的方法中,就可以重用这些验证的逻辑,原来代码中的isEmailAvailable()方法就是工具方法层面的复用。比如说常见的代码中的很多的工具类,如StringUtilsDateUtils,就是这样的一个思路,实现了一些工具方法层面的重用。(对于Ruby,Kotlin,可以非常优雅的扩展父类方法)。

不过这部分重构,我并非仅仅是抽取了几个验证的方法。下面会描述,这里先说一下我考虑的两个原则:

  1. 尽可能的代码复用
  2. 为使用方提供一致的调用外观

提供较为一致的调用外观

这部分主要是真的按钮的响应,所以这里统一了方法handleOperation的调用,然后通过type来区分。这部分看个人的习惯了,比如原来的方式,也是不错的,方法名就反映出来该方法的意图。

<el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openAmlAccount')"><span>开通反洗钱</span></el-button>
<el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openAmlInviteAccount')"><span>开通反洗钱邀请账号</span></el-button>
<el-button type="danger"  :disabled="totalSelectedRows === 0" @click="handleOperation('deleteOpportunities')"><span>批量删除</span></el-button>
<el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openNormalAccount')"><span>开通账号</span></el-button>
<el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openInviteAccount')"><span>开通邀请账号</span></el-button>
<el-button type="danger" :disabled="totalSelectedRows === 0" @click="handleOperation('physicallyDelete')"><span>删除账号</span></el-button>
<el-button type="danger" :disabled="totalSelectedRows === 0" @click="handleOperation('completelyPhysicalDelete')"><span>彻底删除</span></el-button>

handleOperation则根据不同的type,分别调用相对于的处理方法。

handleOperation(type) {
    console.log(type);
}

尽可能的代码复用

针对代码复用的部分,这边则主要的就是那些验证的部分,验证公司名称不能为空,验证销帮帮ID不能为空等。我这里并没有采用工具方法,而且采用了更加面向对象的方式,比如验证公司名称,创建了相应的验证类CompanyNameValidator.js,实现如下:(我这里的名称其实并不是太好,因为没有表达出该验证类的意图 - 公司名称不能为空,CompanyNameCannotBeNullValidator会更加的合适一点)

/**
*  CompanyNameValidator.js
 * 公司名称不能为空
 */
var CompanyNameValidator = {
    validate: function (index, item) {
        if (!item.companyName) {
            return {
                msg: "第" + (index + 1) + "行数据没有公司名称",
                error: true,
            }
        } else {
            return {
                error: null
            }
        }
    }
};

export default CompanyNameValidator;

另外一个验证邮箱的实现,

/**
*  EmailValidator .js
 * 验证Email的格式
 */
var EmailValidator = {
    validate: function (index, item) {
        if (!isEmailValid(item.email)) {
            return {
                msg: "第" + (index + 1) + "行数据的【邮箱格式】有误,公司名称为:" + item.companyName,
                error: true
            }
        } else {
            return {
                error: null
            }
        }
    }
};

export default EmailValidator;

然后我可以统一导出这些验证器:

export {default as CompanyNameValidator} from './CompanyNameValidator'
export {default as XBBIdValidator} from './XBBIdValidator'
export {default as EmailValidator} from './EmailValidator'
export {default as PhoneNumberValidator} from './PhoneNumberValidator'
export {default as NormalOpportunityValidator} from './NormalOpportunityValidator'

这样,这些验证器就可以复用了:

import {CompanyNameValidator, XBBIdValidator, EmailValidator, PhoneNumberValidator, NormalOpportunityValidator} from './validators';
源文件结构

如果需要增加新的功能,需要引入新的验证器,同样的增加一个,然后export导出就可以了。
验证器的使用:

handleOperation(type) {
    console.log(type);
    let selectedRows = this.selectedRows;
    let validators = [CompanyNameValidator];

    let errors = [];
    selectedRows.forEach((row, index) => {
        validators.forEach(validator => {
            let result = validator.validate(index, row);
            if (result.error != null) {
                errors.push(result);
            }
        });
    });

    if (errors.length > 0) {
        errors.forEach(error => {
            this.$message({
                type: 'info',
                message: error.msg
            });
        });
    } else {
        console.log('--------------------------------------')
    }
}

然后进一步的将验证器使用部分封装起来,

var ValidatorUtil = {
    validate: function(selectedRows, validators) {
        let errors = [];
        selectedRows.forEach((row, index) => {
            validators.forEach(validator => {
                let result = validator.validate(index, row);
                if (result.error != null) {
                    errors.push(result);
                }
            });
        });

        return errors;
    }
};

export default ValidatorUtil;

使用部分,只需要调用调用该工具:

import ValidatorUtil from './validators/ValidatorUtil';
....
ValidatorUtil.validate(selectedRows, validators);

所以使用部分,则进一步的简化了:

handleOperation(type) {
    ......
    let selectedRows = this.selectedRows;
    let validators = [CompanyNameValidator, XBBIdValidator, EmailValidator];

    let errors = ValidatorUtil.validate(selectedRows, validators);

    if (errors.length > 0) {
        errors.forEach(error => {
            this.$message({
                type: 'info',
                message: error.msg
            });
        });
    } else {
        console.log('--------------------------------------')
    }
}

接下来就是不同的type的不同实现了,由于验证器部分已经可以达成复用,上述handleOperation已然是一个模板了,不同的方法,只要传入不同的validators数组就行了。

重构后的代码

根据上面的重构思路和描述的过程,贴出一部分重构后的代码:

handleOperation(type) {
    let selectedRows = this.selectedRows;

    switch (type) {
        case 'openAmlAccount':
            this.processOpenAmlAccount(selectedRows);
            break;
        case 'openAmlInviteAccount':
            this.processOpenAmlInviteAccount(selectedRows);
            break;
        case 'deleteOpportunities':
            this.processDeleteOpportunities(selectedRows);
            break;
        case 'openNormalAccount':
            this.processOpenNormalAccount(selectedRows);
            break;
        case 'openInviteAccount':
            this.processOpenInviteAccount(selectedRows);
            break;
        case 'physicallyDelete':
            this.processPhysicallyDelete(selectedRows);
            break;
        case 'completelyPhysicalDelete':
            this.processCompletelyPhysicalDelete(selectedRows);
            break;
    }
}
/**
 * 开通账号
 * @param selectedRows
 */
processOpenNormalAccount(selectedRows) {
    let validators = [CompanyNameValidator, XBBIdValidator, EmailValidator, PhoneNumberValidator];
    this.process('OpenNormalAccount', '开通账号', selectedRows, validators);
},

/**
 * 开通邀请账号
 * @param selectedRows
 */
processOpenInviteAccount(selectedRows) {
    let validators = [CompanyNameValidator, XBBIdValidator, PhoneNumberValidator];
    this.process('OpenInviteAccount', '开通邀请账号', selectedRows, validators);
},

/**
 * 删除账号
 * @param selectedRows
 */
processPhysicallyDelete(selectedRows) {
    let validators = [NormalOpportunityValidator];
    this.process('PhysicallyDelete', '删除账号', selectedRows, validators);
},


/**
 * 批量处理公共执行方法
 *
 * @param type                   批量操作类型
 * @param processMsg     操作说明,操作成功后提示信息
 * @param selectedRows  选中的表格行数据
 * @param validators        该操作涉及的验证器集合
 */
process(type, processMsg, selectedRows, validators) {
    let errors = ValidatorUtil.validate(selectedRows, validators);
    if (errors.length > 0) {
        errors.forEach(error => {
            this.$message({
                type: 'error',
                message: error.msg
            });
        });
    } else {
        let datas = [];
        selectedRows.forEach(row => {
            datas.push(
                {
                    opportunityId: row.id,
                    companyName: row.companyName,
                    customerId: row.customerId,
                    contactNumber: row.contactNumber,
                    email: row.email
                }
            )
        });

        this.$axios.post('/api/opportunities/batch', {
            type: type,
            data: datas
        }, {}).then(res => {
            if (res.status === 200) {
                this.doSearch();
                this.$message({
                    type: 'success',
                    message: processMsg + '操作成功!'
                });
            }
        }).catch(error => {
            this.$message({
                type: 'error',
                message: processMsg + '操作失败! ' + error.msg
            });
        })
    }
}

后记

计划这一篇是前端部分,不过自己接触前端(Javascript, Vue)并没有多久,主要其实还是写后端代码,所以主要还是希望把意图表达清楚。下面则将视角稍微往上一点,首先从功能实现角度,都是工作的,所以这里也还是已技术为主要视角。

主要的套路(模式)见下面的UML类图,这是个很实用的套路,很多场景下都可以使用,下一篇将写一下后端代码(Java)部分的重构,其实也是基于这个套路。


UML类图

谢谢阅读。

Works,then better.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • 以下文章转载自知乎,暗灭-京华九月秋近寒,浮沉半生影长单. 暗灭 京华九月秋近寒,浮沉半生影长单 10,850 人...
    ve追风_685b阅读 4,084评论 1 15
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,449评论 1 45
  • 001 切勿“以己之短,克人之长” “梅须逊雪三分白,雪却输梅一段香”,喜欢顾影自怜的人,不要和别人比,列出自己的...
    心如采薇阅读 494评论 5 3
  • 关键词:飞瀑 梦想 爱 他小时候的梦想是做一名如他父亲一样英勇而足智多谋的建筑师。可是,那年的入学考试,一...
    农夫山泉水阅读 193评论 0 0