时间: 2020-09-03 00:08:26 人气: 2281 评论: 0
作者:孟健, 腾讯web前端开发 工程师
商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。
原文链接:http://wetest.qq.com/lab/view/384.html
WeTest 导读
写这篇文章时内心是比较忐忑的,因为文章的话题范围非常大,怕自己驾驭不了。在实际工作中,维护过很多类型的代码,其中不乏高级工程师完成的逻辑,大家的需求能力都很不错,能够快速满足产品的需要,但很少能有人能注意到代码的整洁度,甚至很多代码经过多人维护后已经变得无法再进行任何一处的修改,最后不得不花大量的时间进行重构。因此我决定还是写一篇文章来“浅谈”软件工程师应具备的代码素养,希望能够对大家有所帮助,水平所限,如有不当之处还请不吝指正~
“程序是写给人读的,只是偶尔让计算机执行一下。” ——Donald Ervin Knuth(高德纳)
一、关于代码素养
我们常常谈到“素养”一词,是指个人在专业领域内实践训练而成的一种修养,在不同的领域中有不同的体现,如在音乐领域中,“音乐素养”是指个人对于音乐的感觉程度,对音高节奏的把控,对不同流派音乐的鉴赏能力等,而在编程领域,也有不同的素养,反映出对基本功、代码整洁度、专业态度等等方面,所谓“代码素养”,简单来说,就是指代码写的是否优雅美观可维护。
绝对完美的代码是不存在的,代码素养并不是指完美主义。在翻译领域有“信,达,雅”的标准,“雅”之所以放在最后,是因为要达到它,需要有比较高的水准和经验积累。类比到编程领域,我们在编程时,第一时间想到的是如何将业务逻辑实现出来,而不是如何把代码优雅地写出来,所以写代码没有所谓的绝对优雅。但是,作为一名专业的前端工程师,确切的说,应该是专业的软件工程师,编写优雅的代码应当是时刻保持的追求,它更像是一个准绳,如同每个人知道自己该做什么,不该做什么,所谓原则,所谓底线,体现出所谓的“代码素养”。
二、破窗理论
破窗理论,原义指窗户破损了,建筑无人照管,人们放任窗户继续破损,最终自己也参与破坏活动,在外墙上涂鸦,任垃圾堆积,最后走向倾颓。
破窗理论在实际中非常容易出现,往往第一个人的代码写的不好,第二个人就会有类似“反正他已经写成这样了,那我也只能这样了”的思想,导致代码越维护越冗杂,最后一刻轰然坍塌,变成无人想去维护的垃圾。
三、整洁的代码
整洁的代码如同优美的散文,试想读过的一本好书,能够随着作者的笔锋跌宕起伏,充满了画面感,调动了自己的喜怒哀乐。代码虽然没有那样的高潮迭起,但整洁的代码应当充满张力,能够在某一时刻利用这种张力将情节推向高潮。
我更喜欢把写代码类比于写文章讲故事,写代码是创作的过程,作者需要将自己想表达的东西通过代码的形式展现出来,而整洁的代码如同讲故事一般,娓娓道来,引人入胜,不好的代码则让人感觉毫无头绪,通篇不知道在讲什么。
四、整洁代码原则
在现代化的前端开发中,有很多自动化工具可以帮助我们写出规范的代码,如eslint,tslint等各种辅助校验工具,知名的规范如google规范、airbnb规范等等也从各个细节方面约束,帮助我们形成合理规范的代码风格。
本小节不再重复语言层面的代码风格,根据实际重构项目,总结出一系列开发过程中需要时刻注意的原则,按照重要程度优先级排列。
相信作为一名软件工程师,大家都听说过最基本的DRY原则,很多设计模式,包括面向对象本身,都是在这条原则上做努力。
DRY顾名思义,是指“不要重复自己”,它实际上强调了一个抽象性原则,如果同样或类似的代码片段出现了两次以上,那么应该将它抽象成一个通用方法或文件,在需要使用的地方去依赖引入,确保在改动的时候,只需调整一处,所有的地方都改变过来,而不是到每个地方去找到相应的代码来修改。
在实际工作中,我见过两种在这条原则上各自走向极端的代码:
● 一种是完全没有抽象概念,重复的代码散落在各处,更奇葩的是,有一部分的抽象,但更多的是重复,比如在common下抽取了一个data.js的数据处理文件,部分页面中引用了该文件,而更多页面完全拷贝了该文件中的几个不同方法代码。而作者的意图则是令人啼笑皆非——只用到小部分代码,没必要引入那么整个文件。且不论现代化的前端构建层面可以解决这个问题,即使是引入了整个大文件,这部分多余的代码在gzip之后也不会损失多少性能,但这种到处copy的行为带来后续的维护成本是翻倍的。
● 对于这种行为还遇到另外一个理由,就是工期时间短,改不动之前的代码,怕造成外网问题,那就拷贝一份相同的逻辑来修改。比如支付逻辑,原有的逻辑为单独的UI浮层+单个支付购买,现在产品提出“打包购买”需求,原有的代码逻辑又比较复杂,出现了“改不动”的现象,于是把UI层和购买逻辑的几个文件整个拷贝过来,修改几下,形成了新的“打包购买”模块,后来产品又提出“按条购买”,按照上述”改不动“原则,又拷贝了一份“按条购买”的模块。这样一来调用处的逻辑就会冗余重复,需要根据不同的购买方式引入不同UI组件和支付逻辑,另外如果新添需求,如支持“分期付款”,那么将改动的是非常多的文件,最可悲的是,最后想要把代码重构为一处统一调用的人,将会面对三份“改不动”的压力,需要众多逻辑中对比分析提取相同之处,工作量已经不能用翻倍来衡量,而这种工作量往往无法得到产品的认同和理解。
● 另一种极端是过度设计,在写每个逻辑的时候都去抽象,让代码的可读性大大下降,一个简单的for循环都要复用,甚至变量定义,这种代码维护起来也是比较有成本的,还有将迥然不同的逻辑过度抽象,使得抽象方法变得非常复杂,经常“牵一发而动全身”,这种行为也是不可取的。
这也是将该原则排在首位的原因,这种行为导致的重构工作量是最大的,保持良好的代码维护性是一种素养,更是一种责任,如果自己在这方面逃避或偷懒,将把这块工作量翻倍地加在将来别人或自己的身上。
SRP也是一个比较著名的设计原则——单一职责,在面向对象的编程中,认为类应该具有单一职责,一个类的改变动机应当只有一个。
对于前端开发来说,最需要贯彻的思想是函数应当保持单一职责,一个函数应当只做一件事,这样一来是保证函数的可复用性,更单一的函数有更强的复用性,二来可以让整体的代码框架更加清晰,细节都封装在一个个小函数中。另外一点也和单一职责有关,就是无副作用的函数,也称纯函数,我们应当尽量保证纯函数的数量,非纯函数是不可避免的,但应当尽量减少它。
把SRP原则排在第二位,因为它非常的重要,没有人愿意看一团乱麻的逻辑,在维护代码时,如果没有一个清晰的逻辑结构,所有的数据定义、数据处理、DOM操作等等一系列细节的代码全部放在一个函数中,导致这个函数非常的冗长,让人本能地产生心理排斥,不愿去查看内部的逻辑。
所有的复杂逻辑放在一个函数中,相信大家看到这样的代码都会眉头一皱:
show: function(a, b) { if (!isInit) { init(); isInit = true; } // reset this.balance = 0; this.isAllBalance = false; var shouldShowLayer = true, preSelectedTermId = 0, needAddress = course.address_state, showTerms, termsObj; var hasPunish = false; this.course = course = course || {}; opt = opt || {}; opt.showMax = opt.showMax || 6; (isIosApp || b.isIAP) && (usekedian = !0, priceSymbol = '<i class="icon-font i-kedian"></i>'), f.splice(b.showMax), layer.show({ $container:b.$container, content:termSelectorTpl({ terms:f, curTermId:b.curTermId || d, name:a.name, hasPunish:h, userInfo:j }, { renderTime:T.render.time.renderCourseTime, renderCourseTime:renderCourseTime, hideUserInfo:b.hideUserInfo, hideTitle:b.hideTitle, hidePayPrice:b.hidePayPrice, confirmText:b.confirmText, sys_time:a.sys_time }), cls:"term-select-new", allowMove:function(a) { return opt.allowMove || ($target.closest('.select-content').length && $('.term-select-new .select-time').height() + $('.term-select-new .select-address').height() + $('.term-select-new .select-discounts').height() > (winWidth > 360 ? 190 : winWidth > 320 ? 175 : 150)); }, afterInit:function(c) { if (needAddress) { that.loadAddress(); // 如果需要地址,且是 app 的话,屏幕可见性切换时需要更新下地址 if (isApp) { $(document).on(visibilityChange, function (e) { // console.log('visibilityChange',document[hidden]); if (!document[hidden]) { // true 参数表示必须刷新 that.loadAddress(true); } }); } } that.afterTermSelect(); $dom.on('click', '.layer-close', function() { setTimeout(function() { !opt.noAutoHide && layer.hide(); }, 100); opt.onCancel && opt.onCancel(); }); $dom.on('click', '.term', function(e) { var $this = $(this); var $terms = $('.term'); if (!$this.hasClass('disabled')) { $terms.removeClass('selected'); $this.addClass('selected'); } that.afterTermSelect(); }); $dom.on('click', '.layer-comfirm', function(e) { var $this = $(this); var termId = $dom.find('.term.selected').data('term-id'); var termName = $dom.find('.term.selected').find('.term-title').html(); var discountId = $dom.find('.discounts-list_item.selected').data('discount-id'); var couId = $dom.find('.discounts-list_item.selected .discounts-coupon').data('cou-id'); var directPay = false; // ios 手Q IAP if (that.toRecharge) { // 需要充值的金额数目 var toRechargePrice = that.curPrice - that.balance; if (isIosApp) { require.async('api', function (api) { api.invoke('api', 'balanceRecharge', { amount: toRechargePrice }); // 充值完成设置回调 api.addEventListener('balanceRechargeCallBack', function(data) { // 支付成功的话 // code=0为成功,其他表示失败 // mode=1表示走充值档位回调,2表示直接充值回调,如果ios 直接充值成功则直接支付 var directPay = data.code === 0 && data.mode === 2; // 执行回调刷新数据 that.toGetBalance(that.course, termId, function() { directPay && $this.trigger('click'); }); }); }); } else { var toRechargePrice = that.curPrice - that.balance; if (that.rechargeMap && Object.keys(that.rechargeMap).indexOf("" + toRechargePrice) > -1) { that.opt.onComfirmClick && that.opt.onComfirmClick(1); iosPay.iosRecharge({ productId: that.rechargeMap[toRechargePrice], count: toRechargePrice, succ: function() { that.toGetBalance(that.course, $('.term.selected').data('term-id')); } }); } else { that.opt.onComfirmClick && that.opt.onComfirmClick(2); // T.jump('/iosRecharge.html?_bid=167&_wv=2147483651'); that. 技术沙龙 教程文章 热点综合