今天读了曾探的《设计模式》 中的代码重构这一章节, 知识点不多却非常实用, 在实际项目中除了使用设计模式重构代码外, 还有一些容易忽略的细节. 这些细节也是重构的重要手段. 写这篇文章总结一下.
代码是写给人看的, 顺便给机器运行. 优雅的代码应该是简单、易维护、可扩展的.
提炼函数 如果一个函数过长, 而且需要注释才清楚它是如何工作的, 那么需要考虑把它独立出来. 这样做的好处是:
避免超大函数, 作用域过大变量不好维护
抽离公共逻辑, 易于复用和覆写
良好的命名起到了注释作用
例子:
1 2 3 4 5 6 7 8 9 const getUserInfo = () => { fetch('https://xxx.com/userInfo' , (data) => { console .log(` userId: ${data.userId} \n userName: ${data.userName} \n nickName: ${data.NickName} ` ); }); };
重构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 const printDetail = (data ) => { console .log(` userId: ${data.userId} \n userName: ${data.userName} \n nickName: ${data.NickName} ` );} const getUserInfo = () => { fetch('https://xxx.com/userInfo' , (data) => { printDetail(data); }); };
⤴️Go Top
合并重复的条件片段 如果一个函数体内有一些条件分支, 而且这些条件分支内散布了一些重复的代码, 这个时候就有必要进行合并去重操作.
例子:
1 2 3 4 5 6 7 8 9 10 11 const paging = (currPage ) => { if (currPage <= 0 ) { currPage = 0 ; jump(currPage); } else if (currPage >= totalPage) { currPage = totalPage; jump(currPage); } else { jump(currPage); } };
可以看到上面的 jump(currPage)
重复了, 完全可以把它独立出来:
1 2 3 4 5 6 7 8 const paging = (currPage ) => { if (currPage <= 0 ) { currPage = 0 ; } else if (currPage >= totalPage) { currPage = totalPage; } jump(currPage); }
⤴️Go Top
把条件分支语句提炼成函数 复杂的条件分支语句是导致程序难以阅读和理解的重要原因, 而且很容易导致一个庞大的函数. 这种情况下就需要把条件分支语句提炼出来.
例子:
1 2 3 4 5 6 7 8 9 const getPrice = (price ) => { const date = new Date (); if (date.getMonth() >= 6 && date.getMonth() <= 9 ) { return price * 0.8 ; } return price; };
观察这段代码:
1 2 3 if (date.getMonth() >= 6 && date.getMonth() <= 9 ) { }
我们需要让它更符合语义, 这样读代码的人能很轻松理解意图, 这里就可以提炼成一个单独函数.
1 2 3 4 5 6 7 8 9 10 11 12 const isSummer = () => { const date = new Date (); return date.getMonth() >= 6 && date.getMonth() <= 9 ; }; const getPrice = (price ) => { const date = new Date (); if (isSummer()) { return price * 0.8 ; } return price; };
⤴️Go Top
合理的使用循环 在函数体内, 如果一些代码只是完成一些重复的工作, 那么可以合理利用循环完成同样的功能, 这样可以保持代码量更少.
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var createXHR = function ( ) { var xhr; try { xhr = new ActiveXObject('MSXML2.XMLHttp.6.0' ); } catch (e) { try { xhr = new ActiveXObject('MSXML2.XMLHttp.3.0' ) } catch (e) { xhr = new ActiveXObject('MSXML2.XMLHttp' ); } } return xhr; }; var xhr = createXHR();
这个时候, 使用循环来优化上面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 var createXHR = function ( ) { var versions = ['MSXML2.XMLHttp.6.0' , 'MSXML2.XMLHttp.3.0' , 'MSXML2.XMLHttp' ]; for (var i = 0 , version; version = versions[i++];) { try { return new ActiveXObject(version); } catch (e) {}; } }; var xhr = createXHR();
⤴️Go Top
提前让函数退出代替嵌套条件分支 很多程序员👨💻都有这种观念 “每个函数只能有一个入口和一个出口”, 但是关于 “函数只有一个出口” 往往有一些不同的看法, 下面用代码来说明一下, 光讲概念有点晦涩.
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 const del = (obj ) => { let ret; if (!obj.isReadOnly) { if (obj.isFolder) { ret = delFolder(obj); } else if (obj.isFile) { ret = delFile(obj); } } return ret; };
嵌套的条件分支绝对是代码维护者的噩梦, 多层嵌套的条件更加不容易理解, 有时候如果代码过长, 上一个 if ()
语句可能相隔很远. 严重影响了阅读体验啊. 重构的 del
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 const del = (obj ) => { if (obj.isReadOnly) { return ; } if (obj.isFolder) { return delFolder(obj); } if (obj.isFile) { return delFile(obj); } };
⤴️Go Top
传递对象参数代替过长的参数列表 有时候一个函数可能接收多个参数, 而且参数越多, 就越难理解和使用. 最重要的一点需要注意参数的顺序, 如果不传的情况要使用占位符代替. 如果参数超过 3 个, 请使用对象吧.
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const setUserInfo = (id, name, address, sex, mobile, qq ) => { } setUserInfo(1 , 'ifyour' , undefined , 'male' , undefined , '123456' ); setUserInfo({ id: 1 , name: 'ifyour' , sex: 'male' , qq: '123456' , })
⤴️Go Top
尽量减少参数数量 如果调用一个函数, 需要传递很多参数, 这样的函数用起来就要小心了, 必须搞清楚每个参数的含义, 所以, 如果能尽量少传参数就少传参数. 把复杂的逻辑封装到函数内部.
例子:
1 2 3 4 5 6 7 8 9 10 11 const draw = (width, height, square ) => { }; const draw = (width, height ) => { const square = width * height; }
这里的 square
参数没必要, 可以通过 width
和 height
计算获得, 还有一种情况, 这个画图函数, 如果可以画多种图形呢? 所以需要在内部去处理, 什么时候需要 square
什么时候需要 radius
, 然后可以使用 策略模式 让它支持画多种图形.
⤴️Go Top
少用三目运算 三目运算能减少代码量, 但是不能滥用, 如果以牺牲代码可读性为代价, 那就得不偿失了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const global = typeof window !== 'undefined' ? window : this ;if (!aup || !bup) { return a === doc ? -1 : b === doc ? 1 : aup ? -1 : bup ? 1 : sortInput ? (indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) : 0 ; }
⤴️Go Top
合理使用链式调用 jQuery 中的链式调用用起来还是很爽的, 它的实现也非常简单, 我们可以很容易的实现一个链式调用. 使用的前提就是链条的结构相对来说稳定, 不容易发生修改, 如果是经常发生修改的话, 还是建议使用普通的调用. 因为它调试的时候需要把这个链子拆开, 才知道是哪个环节有 Bug.
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var User = function ( ) { this .id = null ; this .name = null ; }; User.prototype.setId = function (id ) { this .id = id; return this ; } User.prototype.setName = function (name ) { this .name = name; return this ; } new User() .setId(1 ) .setNmae('ifyour' );
或者这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var User = { id: null , name: null , setId: function (id ) { this .id = id; return this ; }, setName: function (name ) { this .name = name; return this ; } }; User.setId(1 ).setName('ifyour' );
⤴️Go Top
分解大型类 从字面意思我们就能理解, 如果一个类里面有太多的方法, 我们要考虑把其中同一类型相关的方法抽离出来. 模块化的好处不言而喻, 把逻辑复杂的分离成小的单元, 这样更便于维护, 出现 bug 了也好定位问题. 来看一段代码.
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var Spirit = function ( ) { this .name = name; }; Spirit.prototype.attack = function (type ) { if (type === 'waveBoxing' ) { console .log(this .name + ': 使用波动拳' ); } else if (type === 'whirlKick' ) { console .log(this .name + ': 使用旋风腿' ); } } var spirit = new Spirit('RYU' );spirit.attack('waveBoxing' ); spirit.attack('whirlKick' );
面向对象的设计鼓励将 行为 分解到合理数量的更小的对象中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var Attack = function (spirit ) { this .spirit = spirit; }; Attack.prototype.start = function (type ) { return this .list[type].call(this ); } Attack.prototype.list = { waveBoxing: function ( ) { console .log(this .sprite.name + ': 使用波动拳' ) }, whirlKick: function ( ) { console .log(this .sprite.name + ': 使用旋风腿' ) } }
现在 Sprite 类精简了很多, 不再直接包含攻击相关的方法, 而是把方法委托给 Attack 类来执行. 这段代码也是 策略模式 的运用之一, 看代码:
1 2 3 4 5 6 7 8 9 10 11 12 var Spirit = function ( ) { this .name = name; this .attackObj = new Attack(this ); }; Spirit.prototype.attack = function (type ) { this .attackObj.start(type); } var spirit = new Spirit('RYU' );spirit.attack('waveBoxing' ); spirit.attack('whirlKick' );
⤴️Go Top
退出多重循环 使用 return
退出多重循环. 一定程度上能简化代码, 使代码更容易理解.
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var func = function ( ) { var flag = false ; for (var i = 0 ; i < 10 ; i++) { for (var j = 0 ; j < 10 ; j++) { if (i * j > 30 ) { flag = true ; break ; } } } }; var func = function ( ) { for (var i = 0 ; i < 10 ; i++) { for (var j = 0 ; j < 10 ; j++) { if (i * j > 30 ) { return ; } } } };
直接使用 return
后, 后面的代码无法执行, 还是有点小问题的:
1 2 3 4 5 6 7 8 9 10 var func = function ( ) { for (var i = 0 ; i < 10 ; i++) { for (var j = 0 ; j < 10 ; j++) { if (i * j > 30 ) { return ; } } } console .log(i) };
为了解决这个问题, 我们可以把相关的代码放到 return
后面. 如果需要执行的代码较多, 可以提取成一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 var print = function (i ) { console .log(i) }; var func = function ( ) { for (var i = 0 ; i < 10 ; i++) { for (var j = 0 ; j < 10 ; j++) { if (i * j > 30 ) { return print(i); } } } };
⤴️Go Top