您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
从代码圈复杂度控制到TDD,我们怎么把一个功能拆分到极致
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
从代码圈复杂度控制到TDD,我们怎么把一个功能拆分到极致
自猿其说Tech
2021-07-27
IP归属:未知
57480浏览
前端
### 1 背景 当我们接到一个新的需求时, 可能都产生过下面的想法: - 重构项目还不如重新开发一个项目 - 这代码是谁写的,这么... 当需求开发完成后,可能又会产生下面的问题: - 应该用什么来量化一个项目的代码质量呢? - 代码重构后,用什么来衡量代码质量是否提升了呢? 应该从哪些方面去解决上面的问题呢?本文将会与大家分享在我们团队在Elsa插件开发过程中,解决以上问题的一些实践。 ### 2 认识Elsa插件 —— 下载物料功能 Elsa是一个智能的可视化开发插件,旨在帮助开发人员更快速、更轻松更便捷的开发前端应用。主要包括自定义组件、可视化拼装表单页面、创建项目模板、添加物料等功能,功能详情见Elsa 官网。本文中涉及到的代码是添加物料功能。 ![](//img1.jcloudcs.com/developer.jdcloud.com/4fcd54b3-66f7-41d2-b63b-09bd008a992420210727140355.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/5f12820b-0934-425a-b995-ead1d79b9ed320210727140413.png) 如上是**下载物料**功能,当我们打开vue文件时,鼠标右键选择添加组件命令,vscode 编辑器右侧会展示物料库面板,选择JyTable组件后点击添加,会自动向该文件中写入以下三条语句,不用像之前,需要手动写入。 ```javascript // template中 <JyTable /> // script中 import JyTable from './components/Jytable' components: {JyTable} ``` ### 3 拆分功能 理解需求之后,我们就要开始考虑开发该功能中会遇到的场景了。页面中的内容不同,插入的内容也不同。细分为如下几种场景: - template部分 存在template,直接在光标位置插入组件名称<JyTable /> 不存在,在第一行插入以下内容<template><div><JyTable /></div></template> - script部分 不存在script,找到最后一个</template>出现的行数,在其下一行插入以下内容: ```javascript <script> import JyTable from './components/JyTable' export default { components: { JyTable } } </script> ``` 存在script,判断是否存在export default语句: A)不存在,判断script标签是否在同一行,在同一行则插入带换行符的export default语句;不在同一行,则在script标签的下一行插入export default语句; B)存在, 判断是否存在components语句: a)不存在,判断export default 语句是否在同一行,在同一行则插入带有换行符的components语句;反之插入没有换行符的components语句; b)存在,判断components语句是否存在同一行,在同一行则插入带有换行符的JyTable语句;反之插入不带换行符的。 ### 4 代码实现 根据上面列出来的场景,实现了如下的代码: ```javascript function insertCode(fsPath, activeTextEditor, componentName) { let documentText = activeTextEditor.document.getText(), documentTextArr = documentText.split(/\r\n|\n|\r/gm) // 以换行符切割数组 let templateIndex = documentTextArr.findIndex((str) => str.trim().includes('<template>') ) // 找到script对应行 let scriptIndex = documentTextArr.findIndex((str) => str.trim().includes('<script')) // 找到components行 let compIndex = documentTextArr.findIndex( (str) => str.trim().search('components:') !== -1 ) // 创建export并插入 let exportIndex = splitArr.findIndex( (str) => str.trim().search('export default') !== -1 ) // 不存在template if (templateIndex === -1) { const template_vueFile = ` <template> <div> <${componentName} /> </div> </template>` editBuilder.insert(new Position(0, 0), template_vueFile) } else { const { selection } = activeTextEditor if (selection && selection.active) { const { line, character } = selection.active const insertPosition = new Position(line, character) editBuilder.insert(insertPosition, util.getTagTemplate(componentName)) } } setTimeout(() => { if (scriptIndex === -1) { documentText = activeTextEditor.document.getText() documentTextArr = text.split(/\r\n|\n|\r/gm) let lastTemplateLabelIndex = 0 documentTextArr.forEach((item, index) => { if (item.includes('</template>')) { lastTemplateLabelIndex = index } }) // ... } else { documentText = activeTextEditor.document.getText() documentTextArr = text.split(/\r\n|\n|\r/gm) documentTextArr.forEach((item, index) => { if (item.includes('<script')) { scriptIndex = index } }) let scriptFlag = splitArr[scriptIndex] .replace(/\s+/g, '') .includes('</script>') if (scriptFlag) { let scriptCol = splitArr[scriptIndex].indexOf('</script>') }) } else { if (exportIndex === -1) { documentTextArr.forEach((item, index) => { if (item.includes('<script')) { scriptIndex = index } }) } else { if (compIndex === -1) { splitArr.forEach((item, index) => { if (item.includes('export default')) { exportIndex = index } if (item.includes('<script>')) { scriptIndex = index } }) activeTextEditor.edit((editBuilder) => { const flag = exportIndex !== -1 && splitArr[exportIndex] .replace(/\s+/g, '') .includes('exportdefault{}') if (flag) { const tempIndex = splitArr[exportIndex].indexOf('{') // ... } else { // ... } // ... }) } else { splitArr.forEach((item, index) => { if (item.includes('components:')) { compIndex = index } if (item.includes('<script>')) { scriptIndex = index } }) activeTextEditor.edit((editBuilder) => { const comFlag = compIndex !== -1 && splitArr[compIndex] .replace(/\s+/g, '') .includes('components:{}') const tempComIndex = splitArr[compIndex].indexOf('{') if(comFlag) { // ... } else { // ... } // ... } } } } }, 1000) } ``` 完成上面的代码后,添加物料的功能就完成了。如果突然增加一个功能,需要改动这段代码,那我们要怎么在实现新功能的同时,又确保已有功能执行结果的正确性呢?—— 那就是增加单元测试。 ### 5 单元测试的实践 #### 5.1 什么是单元测试? 单元测试是针对程序最小单元来进行正确性检验的测试工作。 #### 5.2 单元测试的作用 - 随时验证代码的正确性,通过写测试用例,一次编写,多次运行; - 用例即文档; - 为重构代码保驾护航; - 简化调试过程,如果测试失败,仅需要调试代码中最新修改的内容; 以上,可以看出单元测试不仅能够保证我们在开发新功能的同时,保证已有功能的正确性,还有其它很多的好处,因此对于项目的核心模块,引入单元测试就很有必要了。在Elsa插件中,我们采用Jest单元测试框架来进行单元测试,在项目中使用jest执行以下三个步骤就可以完成: 1. 安装 ```javascript npm install jest -D ``` 2. 在项目根目录下新建jest.config.js文件,写入如下配置: ```javascript module.exports = { testEnvironment: 'node', testRegex: 'tests/.*\\.test\\.js$', collectCoverage: true, coverageDirectory: '<rootDir>/tests/unit/coverage', // 测试报告想要覆盖的文件 collectCoverageFrom: ['src/**/*.js', '!**/node_modules/**'], automock: false, moduleFileExtensions: ['js', 'json', 'ts'], transformIgnorePatterns: ['/node_modules/'], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, } ``` 3. 配置package.json,在scripts字段中增加以下语句 ```javascript "watch-test": "jest --watchAll" ``` #### 5.3 以TDD模式为添加物料功能增加测试用例 经过上面的配置,直接在命令行中输入npm run watch-test命令就能够自动执行所有的测试用例。单元测试主要和项目根目录下的tests文件夹相关,所有的测试用例都存放于该文件夹下的xx.test.js文件中。比如针对于插入template的两种场景: - 存在template,直接在光标位置插入组件名称<JyTable /> - 不存在,在第一行插入以下内容<template><div><JyTable /></div></template> 新建insertCode.test.js ```javascript import { insertCode } from 'src/insertCode.js' it('insert template label in position 0 line', () => { const documentText = '' const insertPosition = new Position(0, 0) const insertContent = createTemlate(componentName, 'template') insertTemplate(documentText, componentName) expect(editBuilder.insert).toHaveBeenNCalledWith(insertPosition, insertContent) }) it('insert componentLabel in thrid line when template is exists', () => { const documentText = ` <template> <div> </div> </template> ` const insertPosition = new Position(2, 10) const insertContent = createVueTemplate(componentName, 'componentLabel') insertTemplate(documentText, componentName) expect(editBuilder.insert).toHaveBeenNCalledWith(insertPosition, insertContent) }) ``` 增加单元测试之后,我们的基本开发过程就结束了,就可以致力于去解决文章开头所提出的问题。深究这些问题,它们的本质就是代码的质量低、可维护性差、并且没有一个标准去衡量代码的好坏,关于这点,我们团队选择了—— 圈复杂度。 ### 6 衡量代码 —— 圈复杂度 #### 6.1 什么是圈复杂度? 圈复杂度,也称为条件复杂度,是一种代码复杂度的衡量标准,它可以用来衡量一个模块判定结构的复杂程度,也可以理解为覆盖所有可能情况最少使用的测试用例数量。圈复杂度大的说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护,因此它能够作为衡量代码的一个标准。 #### 6.2 衡量标准 代码圈复杂度低不一定是好的代码,但是圈复杂度高的代码一定是难以维护和难以理解的代码。代码和圈复杂度之间的衡量标准如下图: ![](//img1.jcloudcs.com/developer.jdcloud.com/5cd0619b-a7a0-491a-a373-73e31a9c157520210727141356.png) 根据圈复杂度的计算,我们上面实现的insertCode方法,圈复杂度已经达到了20+。如果换一个人来维护这段代码,可能都很难看懂,并且如果出现问题,由于代码复杂,也都很难调试。下面将详细阐述以降低圈复杂度为目的进行的实践。 #### 6.3 基于圈复杂度优化添加物料功能 针对于添加物料功能,我们可以从两个方面降低其圈复杂度: 1. 提炼函数:将特定功能的代码放进一个独立的函数中,并且函数名语义化 (1)提取查找字符串出现的位置索引的方法 ```javascript function findIncludesIndex(keyword, arr = []) { return arr.findIndex((str) => str.trim().includes(keyword)) } ``` (2)将处理template、script、export default、components部分独立成函数 ```javascript function insertTemplate(documentText, componentName) { const documentTextArr = documentText.split(/\r\n|\n|\r/gm) let templateIndex = findIndex('<template>', documentTextArr) let insertPosition = new Position(0, 0) let insertContent = createVueTemplate(componentName).template if (templateIndex !== -1) { const { selection } = activeTextEditor if (selection && selection.active) { const { line, character } = selection.active insertPosition = new Position(line, character) insertContent = createVueTemplate(componentName).labelTemplate } } editBuilder.insert(insertPosition, insertContent) } function insertScript() {} function insertExport() {} funtion insertComponents () {} ``` 2. 替换算法:把当前算法重构成更清晰的算法,如下所示,重构前圈复杂度为7,重构后圈复杂度降低为2。 ```javascript // 修改前 圈复杂度7 function createVueTemplate(type, componentName) { let content = '' switch (type) { case 'template': content = ` <template> <div> <${componentName} /> </div> </template>` break case 'importLabel': content = `import ${componentName} from './components/${componentName}';\n` break case 'wrapComponentLabel': content = `\ncomponents: {\n\t${componentName},\n},\n` break case 'componentLabel': content = `components: {\n\t${componentName},\n},\n` break case 'component': content = `\t${componentName},\n` break case 'wrapComponent': content = `\n\t${componentName},\n` break default: break } return content } // 修改后 圈复杂度为2 function createVueTemplate(type, componentName) { const templates = { template: ` <template> <div> <${componentName} /> </div> </template>`, importLabel: `import ${componentName} from './components/${componentName}';\n`, wrapComponentLabel: `\ncomponents: {\n\t${componentName},\n},\n`, componentLabel: `components: {\n\t${componentName},\n},\n`, component: `\t${componentName},\n`, wrapComponent: `\n\t${componentName},\n` } return templates[type] || '' } ``` 如上,我们主要做了两件事情: - TDD:通过这种模式,在写代码之前对功能代码实现进行设计,以确保测试用例的简单和健壮性,防止代码改动但结果未变的情况下,导致测试用例失败;另外,代码设计也从一定程度上增加了代码的可维护性。 - 圈复杂度:通过降低函数的圈复杂度,让我们的代码变得清晰、高可维护。 ### 7 结语 随着快速发展,对于所有开发人员的标准也从最开始的不论代码质量只要能够实现功能,变成了不仅要实现功能还要保证产出代码的高质量及可维护性。随着项目的庞大,不仅是业务逻辑越来越复杂,参与开发的人员也越来越多,因此代码的高质量和可维护性也越来越重要。为了去做到更好,就需要有一些标准去衡量我们产出的代码和优化代码的方式,如本文中提到的圈复杂度,以帮助我们能够更好的进行团队协作及代码质量。 以上,或有不足,欢迎指正。同时欢迎大家对Elsa插件提出宝贵的建议。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:中台技术部 何婷
原创文章,需联系作者,授权转载
上一篇:企业架构研究: saas3.0-snowflake
下一篇:一次简单的JAVA进程到线程资源使用率异常分析
相关文章
前端十年回顾 | 漫画前端的前世今生
Taro小程序跨端开发入门实战
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号