编写代码时清晰至上

好的代码是清晰的代码 , 而不是聪明的代码
许多程序员尝试编写干净 , 智能的代码 。 但是 , 有时候 , 痴迷于智能可能会使代码库更难以理解 , 并且可能会花费大量时间来阅读和维护它 。
如今 , 在团队合作中 , 人们逐渐意识到编写人工代码的意义 , 这意味着您在编写代码时应该尊重他人 , 而不是炫耀自己的智慧 。 人们正在尝试不要使用"干净"一词 , 因为这意味着即使您不是故意的 , 代码也很脏 。 丹尼尔·欧文(DanielIrvine)在他的文章"干净代码 , 肮脏代码 , 人类代码"中谈到了这一点 。
我并不是说干净是一件坏事 。 在处理个人项目时 , 我会尝试以一种聪明的方式使代码库变得干净 。 但更重要的是 , 我使代码库更具可读性和可理解性 。
正如鲍伯叔叔在他的《清洁守则》中所说:
"总的来说 , 程序员是非常聪明的人 。 聪明的人有时喜欢通过展示他们的心理杂耍能力来炫耀自己的聪明人 。 毕竟 , 如果您可以可靠地记住r是URL的小写版本 , 并且除去了主机和Schema , 那么您显然必须非常聪明 。 聪明的程序员和专业的程序员之间的区别是 , 专业人士理解清晰为王 。 专业人士会尽力而为 , 并编写他人可以理解的代码 。 "-罗伯特·C·马丁
最重要的是编写清晰易懂的代码 。 其他人不仅是其他人 , 而且还是您 , 他们将在几个月内重写代码 。
在本文中 , 我并不是在谈论人的代码方面 。 相反 , 我将通过一些示例来重点介绍如何将该原理应用于您的代码 , 并最大程度地减少花一些时间来理解它的时间 。
注意:为了解释一些技巧 , 我将使用JavaScript或TypeScript 。
涵盖的示例:
·命名
·注释
·条件
·循环
·职能
·测试
命名在软件开发中 , 许多程序员在命名事物时遇到麻烦 。 但是我个人认为 , 关键是避免歧义并使用特定的词语 。
例如:
constfetch=async()=>{returnawaitaxios.get('/users')}constusers=awaitfetch()...在此代码中 , 您可以预期将从服务器获取什么内容 。 但是 , 如果导出了导出功能并在其他文件中使用该怎么办?
exportconstfetch=async()=>{returnawaitaxios.get('/users')}在其他文件中:
import{fetch}from'./utils'fetch()//fetch...what?相反 , 您可以更具体地命名:
exportconstfetchUsers=async()=>{returnawaitaxios.get('/users')}我说过你应该避免歧义 。 请注意以下通用动词:
·set
·get
·group
·begin
·validate
·send
另一个例子:
constxxx=validateForm()在这段代码中 , 您可以理解validateForm是正在验证表单 , 但是您期望返回什么?
但是假设您这样写:
constxxx=isFormValid()然后非常清楚 , 该方法将返回true或false 。
而且 , 如果您这样编写代码 , 则可以假定该方法将返回一个数组或形式错误的映射:
constxxx=getFormErrors()另一个例子:
consttoken=getToken()如您所见 , getToken可能会获得一个令牌 。 但是从什么呢?如果它使用异步功能从服务器获取令牌怎么办?
consttoken=getToken()//usetokenforsomethingdoSomething(token)这可能会在doSomething函数中导致未定义的错误 , 因为在这种情况下 , 您需要等待getToken完成 。
consttoken=awaitgetToken()//usetokenforsomethingdoSomething(token)它工作正常 。 但是getToken在这种情况下不合适 , 因此您可以将其重命名:
consttoken=awaitfetchToken()//usetokenforsomethingdoSomething(token)这样一来 , 更清楚的是该方法将从某些服务器或异步设备中获取令牌 。
为了解决这些问题 , 许多聪明的人提出了一个很好的例子 , 但是重要的是让人们以简单的方式知道它的用途 。
注释通常 , 注释的目的是帮助人们尽可能地了解代码 , 并且注释可以使人们更快地理解代码 。 但是您不必总是对代码发表注释 。 您需要知道毫无价值的注释和良好的注释之间的界限 。
什么没什么好评论如果人们可以轻松理解代码的功能 , 则无需对代码进行注释 。
例如:
//Findstudentfromlists,withthegivenidconststudent=students.find(s=>s.id===id)另一个例子:
//Calculatetaxbasedontheincomeandwealthvalueand....constincome=document.getElementById('income').value;constwealth=document.getElementById('wealth').value;tax.value=https://pcff.toutiao.jxnews.com.cn/p/20200815/(0.15*income)+(0.25*wealth);//...这似乎是解释它是什么的很好的评论 , 但可以进行改进 。
functioncalculateTax(income,wealth){return(0.15*income)+(0.25*wealth);}应将代码块移至函数中 , 并输入一个名称来解释其功能 。 简洁明了的功能名称和自记录功能要好于注释 。
注释什么我们介绍了您不应该注释的代码类型 。 接下来 , 我们将看到应该注释的内容 。
您需要在以下代码处添加注释:
·有缺陷 , 例如性能问题
·可能会导致人们意想不到的行为
·需要进行总结 , 以便人们可以轻松掌握细节
·需要解释为什么有更好的方法时必须以这种方式编写
这些是您在编写代码时想出的宝贵见解 。 如果没有这些注释 , 人们可能会认为存在错误 , 或者应该对代码进行测试或修复 , 这可能会浪费时间 。 为避免这种情况 , 您应该解释为什么以某种方式编写代码 。
重要的是要让自己穿上别人的鞋子 。 提前考虑并预测人们可能会陷入的陷阱 。
条件在编写代码时 , 我们必须处理条件 。 许多if/else语句使您停止阅读代码库 , 而陷入困境 。 我相信条件语句越少 , 代码的可读性就越高 。
通过使用圈复杂度 , 您可以计算代码的复杂度 。 如果在代码中使用了大量的if/else , 循环或switch语句 , 则计数将很高 。 通常 , 计数越高 , 代码越复杂 。
如果您是IntelliJ用户 , 则可以在首选项中检查复杂性:
并且当您在函数中编写许多if/else语句时 , 它会警告您:
关键是尽可能减少不必要的条件 , 并最大程度地减少理解代码的时间 。
以下是一些简化和使条件可读的技巧:
·首先处理正面的案例 , 而不是负面的案例
·早点返回
·使用Array.includes处理多个案例
·使用可选链接处理未定义的检查
首先处理正面 , 而不是负面哪个更适合您阅读?
if(!debug){//dosomething}else{debugSomething()}要么
if(debug){debugSomething()}else{//dosomething}在大多数情况下 , 优先使用正面案例 。 但是 , 如果否定情况是更简单 , 更谨慎的情况 , 则可以这样编写:
if(!user)thrownewError('Pleasesigninfirst')//doalotofthingshere//...早点返回例如:
exportconstformatDate=(date)=>{letresultif(date){constdateObj=newDate(date)if(isToday(dateObj)){result='Today'}elseif(isYesterday(dateObj)){result='Yesterday'}elseif(!isThisYear(dateObj)){result=format(dateObj,'MMMMd,yyyy')}else{result=format(dateObj,'MMMMd')}}else{thrownewError('Nodate')}returnresult}它工作正常 , 但是代码有点长且嵌套 。 而且 , 如果添加了if/else语句 , 将更难弄清楚右括号在哪里 , 并且更难调试代码 。
为了使它看起来更整洁 , 我们需要做的是:
·如果没有日期 , 则抛出错误
·如果日期是今天 , 则返回"今天"
·如果日期是昨天 , 则返回"昨天"
·如果日期不在今年 , 则返回日期和年份
·如果不符合上述条件 , 则返回日期和月份和日期
exportconstformatDate=(date)=>{if(!date)thrownewError('Nodate')//Ifnodate,throwanerrorconstdateObj=newDate(date)if(isToday(dateObj))return'Today'//Ifthedateistoday,return'Today'if(isYesterday(dateObj))return'Yesterday'//Ifthedateisyesterday,return'Yesterday'if(!isThisYear(dateObj))returnformat(dateObj,'MMMMd,yyyy')//Ifthedateisnotinthisyear,returndatewithyearreturnformat(dateObj,'MMMMd')//Ifnomatchingtheabove,returndatewithmonthanddate}看起来更好 。 从函数中多次返回非常适合使代码可读 。
使用Array.includes处理多个案例如果您有多个条件 , 则可以使用Array.includes以避免扩展语句 。
例如:
if(kind==='Persian'||kind==='Maine'||kind==='BritishShorthair'){//dosomething...}考虑到以后可以将其他条件添加到语句中 , 我们想要重构代码 , 如下所示:
constCATS_TYPE=['Persian','Maine','BritishShorthair']if(CATS_TYPE.includes(kind)){//dosomething...}具有类型数组 , 您可以从代码中单独提取条件 。
使用可选链接处理未定义的检查可选的链接允许您深入访问嵌套对象 , 而无需在临时变量中重复分配结果 。 通过使用此选项 , 可以减少条件检查中的多次检查 。
注意:如果要在JavaScript中使用可选的链接运算符 , 则需要安装Babel插件 。 在3.7以上的Typescript中 , 无需任何配置即可使用它 。
例如:
if(user&&user.addressInfo){letzipcodeif(user.addressInfo.zipcode){zipcode=user.addressInfo.zipcode}else{zipcode=''}//dosomething}如果要检查用户是否存在并避免发生未定义的错误 , 则需要编写类似于上面示例的条件 。
但是通过使用可选的链接运算符 , 代码将是:
constzipcode=user?.addressInfo?.zipcode||''这样看起来更好并且更易于维护 。 可以访问内部的嵌套对象并避免发生未定义的错误 。
您可以在TypeScript游乐场中使用此炫酷功能
循环简化循环使您的代码更容易理解 。
在实际情况下 , 您可能会在对象中遇到复杂的嵌套循环 。 如果您有嵌套对象并且必须在todo3中获得列表名称 , 该怎么办:
consttodos=[{code:'code',name:'name',list:[{name:'todoname',},{name:'todoname',},],todo2:[{code2:'code2',name2:'name2',list:[{name:'todoname2',description:'',},{name:'todoname2',description:'',}],todo3:[{code3:'code3',name3:'name3',list:[{name:'todoname3',description:'',},{name:'todoname3',description:'',}]}]}]},]例如 , 您可以这样编写:
constlist=[];todos.forEach(t=>{t.todo2.forEach(t2=>{t2.todo3.forEach(t3=>{t3.list.forEach(l=>{list.push({todo3Name:l.name})})})})})您会得到以下列表:
constlist=todos.reduce((acc,t)=>[...acc,...t.todo2],[]).reduce((acc,t2)=>[...acc,...t2.todo3],[]).reduce((acc,t3)=>[...acc,...t3.list],[]).map(l=>({todo3Name:l.name}))那也很好 。
职能在编写函数时 , 请牢记以下提示:
·使用摘要名称来说明其操作 。
·为一个目的创建一个功能 。
·较小的函数更易读 。
使用摘要名称来说明其操作乍看之下的代码如下 , 您可能会停止阅读并试图弄清楚它在做什么:
consttmp=newSet();constfiltered=lists.filter(a=>!tmp.has(a.code)&&tmp.add(a.code))那呢?
constfiltered=uniqueByCode(lists)您可能会期望有一个函数 , 通过查看对象来删除具有重复代码的对象 。
两者都能很好地工作 , 并获得相同的结果 。
另一个例子:
constperson={score:25};letnewScore=person.scorenewScore=newScore+newScorenewScore+=7newScore=Math.max(0,Math.min(100,newScore));console.log(newScore)//57如果我们为其编写函数 , 则代码将如下所示:
letnewScore=person.scorenewScore=double(newScore)newScore=add(newScore,7)newScore=boundScore(0,100,newScore)console.log(newScore)//57好多了但个人而言 , 我喜欢管道运算符的想法 , 该运算符与将多个函数链接在一起以提高函数编程的可读性一起使用 。
如果我们在JavaScript中使用管道 , 则代码将如下所示:
constperson={score:25};constnewScore=person.score|>double|>add(7,?)|>boundScore(0,100,?);newScore//=>57为一个目的创建功能现在我们了解了摘要名称的重要性 。 但是 , 如果您不能为您的功能起一个好名字怎么办?
例如:
constupdateUser=async(user)=>{try{awaitaxios.post('/users',user)awaitaxios.post('/user/profile',user.profile)constemail=newEmail()awaitemail.send(user.email,'Userhasbeenupdatedsuccessfully')constlogger=newLogger()logger.notify()}catch(e){console.log(e)thrownewError(e)}}您可能想知道它应该是updateUserAndProfile , updateUserAndProfileAndNotify还是其他名称 。 当您陷入困境时 , 就该将代码分成较小的部分了 , 因为人们很难同时理解多条代码 。
当您编写用于更新用户的函数时 , 代码应如下所示:
constupdateUser=async(user)=>{try{awaitaxios.post('/users',user)}catch(e){//handlingerror}}consthandleUpdate=async(user,onUpdated)=>{try{awaitupdateUser(user)awaitupdateProfile(user.profile)awaitonUpdated(user)//emailornotifysomething}catch(e){//handlingerror}}这是一个非常简单的示例 , 但是实际开发中有很多情况 。 要牢记的关键思想是退后一步 , 考虑功能应该做什么 , 并考虑所有问题 , 以便一次只执行一项任务 。
较小的函数更易读当您出于某个目的编写较小的函数时 , 代码将更具可读性和可理解性 。
例如:
constgenerateQuery=(params)=>{constquery={}try{if(params.email){constisValid=isValidEmail(params.email)if(isValid){query.email=params.email}}constdefaultMaxAgeLimit=JSON.parse(localStorage.getItem('defaultMaxAgeLimit')||'')if(params.maxAge){if(params.maxAge<25){query.maxAge=params.maxAge}}else{query.maxAge=defaultMaxAgeLimit}if(params.limit){query.limit=params.limit}//doalotofthingshere//...}catch(err){//errorhanding}returnquery}该函数输出如下:
通常 , 人们一次只能考虑两件事 。 代码表达越大 , 理解和维护就越困难 。
因此 , 使代码更小:
constemail=(query,params)=>{if(!params.email)returnqueryconstisValid=isValidEmail(params.email)if(!isValid)thrownewError('Invalidemail')return{...query,...{email:params.email}}}constmaxAge=(query,params)=>{constobj={maxAge:''}if(!params.maxAge)obj.maxAge=JSON.parse(localStorage.getItem('defaultMaxAgeLimit')||'')if(params.maxAge&&params.maxAge<25)obj.maxAge=params.maxAgereturn{...query,...obj}}constlimit=(query,params)=>{if(!params.limit)returnqueryreturn{...query,...{limit:params.limit}}}constgenerateQuery=(params)=>{letquery={}try{query=email(query,params)query=maxAge(query,params)query=limit(query,params)}catch(err){//errorhanding}returnquery}将巨型代码分解为小段可以使代码更清晰 , 更易读 。 更重要的是 , 每个问题都与其余代码分开 , 因此您可以轻松地调试和测试它 。
如果您还想使其更具通用性和声明性 , 则可以这样编写:
constgenerateQuery=(params,callbacks)=>{letquery={}try{callbacks.forEach(c=>{query=c(query,params)})}catch(err){//errorhanding}returnquery}constresult=generateQuery({email:'xxx@gmail.com',maxAge:20},[email,maxAge,limit])它输出相同的结果 。
这全都与人工密码有关 。 您可以在"再见 , 干净代码"一文中更详细地了解它 。
测试中我不是在谈论TDD , 而是在团队开发中可读性对测试的重要性 。
编写测试非常重要 , 因为:
·在没有文档的情况下 , 您的队友可以通过阅读测试说明轻松了解详细信息 。
·您的队友可以理解真正的代码应该如何工作以及为什么 。
·您的队友可以轻松添加新功能 , 而不必担心会破坏代码 。
·鼓励您的队友添加测试 。 (如果测试代码太大且令人生畏 , 则可能会破窗!)
这些是我个人在团队发展中的经验教训 。 优秀的程序员始终会编写具有良好可维护性的测试 。
这是编写测试时的一些技巧:
·用简单的英语描述测试的目的(最好使用您的母语) 。
·遵循AAA(安排 , 执行 , 声明)模式 。
·使添加测试用例(表驱动的测试模式)变得容易 。
用简单的英语描述测试要做什么就像我在上面说的 , 如果测试是描述性的 , 则人们可以轻松地了解它在做什么 。
假设我们有一个类似util的函数:
exportconstgetAnimal=(code)=>{if(code===1)return'CATS'if(code===2)return'DOGS'if(code===3)return'RABBITS'returnnull}并编写一个测试:
import{getAnimal}from'./util';describe('getAnimal',()=>{it('passes',()=>{expect(getAnimal(1)).toEqual('CATS')})})这是一个非常简单的示例 , 因此您可能可以理解它正在尝试执行的操作 。 但是 , 如果它变大并弄乱了 , 您将很难理解它 。
没关系 , 因为您已经编写了此功能 , 并且知道其功能 。 但是测试不仅适合您 , 还适合您的队友 。
让我们更具描述性:
describe('getAnimal',()=>{it('shouldgetCATSwhenpassingcode1',()=>{expect(getAnimal(1)).toEqual('CATS')})})这看起来有点多余 。 但这不是重点 。 通过描述它 , 您可以使人们知道正确的行为是在代码为1时获取CATS 。
为了使其在每个上下文中都更清楚 , 可以使用上下文块 , 如下所示:
describe('getAnimal',()=>{context('whenpassingcode1',()=>{it('shouldgetCATS',()=>{expect(getAnimal(1)).toEqual('CATS')})})})注意:如果使用Jest , 则可以安装jest-plugin-context 。
通过这样编写 , 您可以在每个块中分隔特定的上下文 。
遵循AAA模式AAA模式允许您将测试分为三个部分:安排 , 操作和声明 。
在安排部分 , 您可以在其中设置数据或模拟要在测试中使用的功能 。
act部分是调用测试方法并在需要时捕获输出值的地方 。
assert部分是您对输出进行声明的地方 。
如果将其应用于上面的示例 , 代码将如下所示:
describe('getAnimal',()=>{context('whenpassingcode1',()=>{beforeEach(()=>{//arrange//preparedatahere})it('shouldgetCATS',()=>{//actconstresult=getAnimal(1)//assertexpect(result).toEqual('CATS')})})})这是使用酶进行反应测试的另一个示例:
describe('Component',()=>{letwrapper:ReactWrapper;beforeEach(()=>{//arrange//mockuseEffectfunctionjest.spyOn(React,'useEffect').mockImplementation(f=>f());});it('shouldrendersuccessfully',()=>{//actwrapper=mount();//assertexpect(wrapper).toMatchSnapshot();});it('shouldupdatethetextafterclicking',()=>{//actwrapper=mount();wrapper.find('button').simulate('click');//assertexpect(wrapper.text().includes("textupdated!"));});});一旦习惯了这种模式 , 就可以更轻松地阅读和理解测试 。
在GitHub上 , javascript-testing-best-practices是解释JavaScript测试的好指南 。
轻松添加测试用例(表驱动测试模式)在Go测试中 , 经常使用表驱动测试模式 。 它的优点是能够通过定义每个表条目中的输入和预期结果来涵盖许多测试用例 。
【编写代码时清晰至上】这是Go中fmt包的示例:
varflagtests=[]struct{instringoutstring}{{"%a","[%a]


    推荐阅读