重构的艺术:五个小妙招助你写出好代码!

开发 前端 开发工具
编写清晰的代码并没有那么复杂。本教程将向你展示五种改进代码的简单技巧,并提供一些实例。

糟糕的代码可以运作,但早晚会让我们付出代价。你有没有遇到过这样的问题:几周后,你无法理解自己的代码,于是不得不花上几个小时,甚至几天的时间来弄清楚到底发生了什么。

解决这个常见问题的方法是使代码尽可能清晰。如果做得更好的话,即使是非技术人员也应该能理解你的代码。

[[281289]]

是时候停止寻找借口,提高我们的代码质量了!

编写清晰的代码并没有那么复杂。本教程将向你展示五种改进代码的简单技巧,并提供一些实例:

1. 不用switch语句

我们通常使用switch语句来代替大型if-else-if语句。但是,switch语句非常冗长,很难维护,甚至很难调试。这些switch语句把我们的代码弄得乱七八糟,而且这些语句的语法很奇怪,很不舒服。在添加更多的case时,我们不得不必须手动添加每个case和break语句,而这就很容易出错。

接下来看一个switch语句的例子:

  1. function getPokemon(type) { 
  2.   let pokemon; 
  3.   switch (type) { 
  4.     case 'Water': 
  5.       pokemon = 'Squirtle'
  6.       break; 
  7.     case 'Fire': 
  8.       pokemon = 'Charmander'
  9.       break; 
  10.     case 'Plant': 
  11.       pokemon = 'Bulbasur'
  12.       break; 
  13.     case 'Electric': 
  14.       pokemon = 'Pikachu'
  15.       break; 
  16.     default: 
  17.       pokemon = 'Mew'
  18.   } 
  19.   return pokemon; 
  20.  
  21. console.log(getPokemon('Fire')); // Result: Charmander 

Switch语句

如果需要在switch语句中添加更多的case的话,需要编写的代码量是相当大的。我们可能最终会复制粘贴代码,而其实我们都知道这种行为的后果是什么。

那么,如何避免使用switch语句呢?可以通过使用对象文本。对象文本简单,易于编写,方便读取,维护轻松。我们都习惯用javascript处理对象,对象文本语法比switch语句更新鲜。下面举个例子:

  1. const pokemon = { 
  2.     Water: 'Squirtle', 
  3.     Fire: 'Charmander', 
  4.     Plant: 'Bulbasur', 
  5.     Electric: 'Pikachu' 
  6.   }; 
  7.  
  8. function getPokemon(type) { 
  9.   return pokemon[type] || 'Mew'; 
  10. console.log(getPokemon('Fire')); // Result: Charmander 
  11.  
  12. // If the type isn't found in the pokemon object, the function will return the default value 'Mew' 
  13. console.log(getPokemon('unknown')); // Result: Mew 

使用对象文本替代switch

如你所见,可以使用运算符 || 添加默认值。如果在pokemon对象中找不到type,getpokemon函数将使mew返回为默认值。

小贴士:你可能已经注意到,我们在函数外部而不是内部声明pokemon对象。这样做是为了避免每次执行函数时都重新创建pokemon。

用映射也能达到同样的效果。映射就像对象一样是键-值对的集合。不同的是映射允许任何类型的键,而对象只允许字符串作为键。此外,映射还有一系列有趣的属性和方法。

以下是使用映射的方法:

  1. const pokemon = new Map([ 
  2.     ['Water', 'Squirtle'], 
  3.     ['Fire', 'Charmander'], 
  4.     ['Plant', 'Bulbasur'], 
  5.     ['Electric', 'Pikachu'] 
  6.   ]); 
  7.  
  8. function getPokemon(type) { 
  9.   return pokemon.get(type) || 'Mew'; 
  10.  
  11. console.log(getPokemon('Fire')); // Result: Charmander 
  12. console.log(getPokemon('unknown')); // Result: Mew 

用映射代替switch语句

如你所见,当用对象文本或映射替换switch语句时,代码看起来更清楚、更直接。

2. 把条件语句写的更有描述性

在编写代码时,条件语句是绝对必要的。然而,他们很快就会失控,最终让我们无法理解这些语句。这导致我们要么必须编写注释来解释语句的作用,要么必须花费宝贵的时间来一条一条检查代码来了解发生了什么。这很糟糕。

看一下下面的语句:

  1. function checkGameStatus() { 
  2.   if ( 
  3.     remaining === 0 || 
  4.     (remaining === 1 && remainingPlayers === 1) || 
  5.     remainingPlayers === 0 
  6.   ) { 
  7.     quitGame(); 
  8.   } 

复杂的条件语句

如果只看前面函数里if语句中的代码,很难理解发生了什么。代码表意不清楚,不清楚的代码只会导致技术错误,还会让人们非常头痛。

怎样改善条件语句呢?可以把它写成一个函数。以下是具体操作方法:

  1. function isGameLost() { 
  2.   return ( 
  3.     remaining === 0 || 
  4.     (remaining === 1 && remainingPlayers === 1) || 
  5.     remainingPlayers === 0 
  6.   ); 
  7.  
  8. // Our function is now much easier to understand: 
  9. function checkGameStatus() { 
  10.   if (isGameLost()) { 
  11.     quitGame(); 
  12.   } 

把条件语句写成函数

通过将条件提取到具有描述性名称isGameLost()的函数中,checkGameStatus函数现在就变得很容易理解。为什么?因为代码表意更清晰。它能够告诉我们发生了什么,这是我们应该一直努力的方向。

[[281290]]

3. 用卫语句(Guard Clauses)代替嵌套的if语句

嵌套if语句是在代码中可能遇到的最可怕的事情之一。你要是想能够完全掌握代码的情况,这绝对会让你精疲力竭。下面是一个嵌套if语句的例子(这个嵌套有三层):

  1. function writeTweet() { 
  2.   const tweet = writeSomething(); 
  3.  
  4.   if (isLoggedIn()) { 
  5.     if (tweet) { 
  6.       if (isTweetDoubleChecked()) { 
  7.         tweetIt(); 
  8.       } else { 
  9.         throw new Error('Dont publish without double checking your tweet'); 
  10.       } 
  11.     } else { 
  12.       throw new Error("Your tweet is empty, can't publish it"); 
  13.     } 
  14.   } else { 
  15.     throw new Error('You need to log in before tweeting'); 
  16.   } 

嵌套的if语句

你可能需要花几分钟时间上下阅读,才能了解函数运作的流程。嵌套的if语句很难让人阅读和理解。那么,如何才能摆脱讨厌的嵌套if语句呢?可以反向思考,使用卫语句来替代这些句子。

“在计算机程序设计中,卫语句是一个布尔表达式,如果程序要在有问题的分支里继续运行的话,它的求值必须为true。”——维基百科

通过颠倒函数的逻辑,并在函数开始时放置导致早期退出的条件,它们将变为保护程序,并且只允许函数在满足所有条件时继续执行。这样可以避免else语句。下面是如何重构之前的函数以使用卫语句的方法:

  1. function writeTweet() { 
  2.   const tweet = writeSomething(); 
  3.  
  4.   if (!isLoggedIn()) { 
  5.     throw new Error('You need to log in before tweeting'); 
  6.   } 
  7.   if (!tweet) { 
  8.     throw new Error("Your tweet is empty, can't publish it"); 
  9.   } 
  10.   if (!isTweetDoubleChecked()) { 
  11.     throw new Error('Dont publish without double checking your tweet'); 
  12.   } 
  13.  
  14.   tweetIt(); 

用卫语句重构函数

如你所见,代码更清晰,更容易理解。我们可以简单向下阅读来了解函数的作用,遵循函数的自然流动,不像以前那样上下阅读。

4. 不要写重复的代码

写重复的代码总是以失败告终。它会导致如下情况:“我在这里修复了这个bug,但是忘记在那里修复”或“我需要在五个不同的地方更改/添加一个新功能”。

正如DRY(Don’t repeat yourself不要重复)原则所说:

每一部分知识或逻辑都必须在一个系统中有单一的、明确的表示。

因此,代码越少越好:它节省了时间和精力,更易于维护,并减少了错误的出现。

那么,如何避免重复代码呢?这有点难,但是将逻辑提取到函数/变量通常效果很好。让我们看看下面的代码,我在重构应用程序时看到了这些代码:

  1. function getJavascriptNews() { 
  2.     const allNews = getNewsFromWeb(); 
  3.     const news = []; 
  4.   
  5.     for (let i = allNews.length - 1; i >= 0; i--){ 
  6.         if (allNews[i].type === "javascript") { 
  7.             news.push(allNews[i]); 
  8.         } 
  9.     } 
  10.   
  11.     return news; 
  12.   
  13. function getRustNews() { 
  14.     const allNews = getNewsFromWeb(); 
  15.     const news = []; 
  16.   
  17.     for (let i = allNews.length - 1; i >= 0; i--){ 
  18.         if (allNews[i].type === "rust") { 
  19.             news.push(allNews[i]); 
  20.         } 
  21.     } 
  22.   
  23.     return news; 
  24.  
  25. function getGolangNews() { 
  26.   const news = []; 
  27.   const allNews = getNewsFromWeb(); 
  28.  
  29.   for (let i = allNews.length - 1; i >= 0; i--) { 
  30.     if (allNews[i].type === 'golang') { 
  31.       news.push(allNews[i]); 
  32.     } 
  33.   } 
  34.  
  35.   return news; 

重复代码示例

你可能已经注意到for循环在这两个函数中完全相同,除了一个小细节:我们想要的新闻类型,即javascript或rust新闻。为了避免这种重复,可以将for循环提取到一个函数中,然后从getJavascriptNews,getRustNews和getGolangNews 函数调用该函数。以下是具体操作方法:

  1. function getJavascriptNews() { 
  2.   const allNews = getNewsFromWeb(); 
  3.   return getNewsContent(allNews, 'javascript'); 
  4.  
  5. function getRustNews() { 
  6.   const allNews = getNewsFromWeb(); 
  7.   return getNewsContent(allNews, 'rust'); 
  8.  
  9. function getGolangNews() { 
  10.   const allNews = getNewsFromWeb(); 
  11.   return getNewsContent(allNews, 'golang'); 
  12.  
  13. function getNewsContent(newsList, type) { 
  14.   const news = []; 
  15.   for (let i = newsList.length - 1; i >= 0; i--) { 
  16.     if (newsList[i].type === type) { 
  17.       news.push(newsList[i].content); 
  18.     } 
  19.   } 
  20.   return news; 

在将for循环提取到getNewsContent函数中之后,getJavaScriptNews, getRustNews和getGolangNews函数变成了简单、清晰的程序。

(1) 进一步重构

但是,你是否意识到,除了传递给getNewsContent的类型字符串之外,这两个函数完全相同?这是重构代码时经常发生的事情。通常情况下,更改一个会导致另一个更改,以此类推,直到重构后的代码最终变成原始代码的一半大小。代码告诉你它需要什么:

  1. function getNews(type) { 
  2.   const allNews = getNewsFromWeb(); 
  3.   return getNewsContent(allNews, type); 
  4.  
  5. function getNewsContent(newsList, type) { 
  6.   const news = []; 
  7.   for (let i = newsList.length - 1; i >= 0; i--) { 
  8.     if (newsList[i].type === type) { 
  9.       news.push(newsList[i].content); 
  10.     } 
  11.   } 
  12.   return news; 

getJavaScriptNews, getRustNews和getGolangNews函数去了哪里?将它们替换为getNews函数,该函数将新闻类型作为参数接收。这样,无论添加多少类型的新闻,总是使用相同的功能。这称为抽象,允许我们重用函数,因此非常有用。抽象是我在写代码的时候最常用的技术之一。

(2) 补充:使用es6特性使for循环更具可读性

for循环并不完全可读。通过引入es6数组函数,可以有95%的机会避免使用它们。在本例中可以使用array.filter和array.map组合来替换原始循环:

  1. function getNews(type) { 
  2.   const allNews = getNewsFromWeb(); 
  3.   return getNewsContent(allNews, type); 
  4.  
  5. function getNewsContent(newsList, type) { 
  6.   return newsList 
  7.     .filter(newsItem => newsItem.type === type) 
  8.     .map(newsItem => newsItem.content); 

用 Array.filter 和 Array.map 来代替循环

  • 用Array.filter只返回其类型等于作为参数传递的类型的元素。
  • 用Array.map只返回item对象的content属性,而不是整个item。

恭喜你,经过三次简单的重构,最初的三个函数已经缩减为两个,这更容易理解和维护。另外,抽象让getNews函数变得可以重新利用。

[[281291]]

5. 一个函数只用来做一件事

一个函数应该只做一件事。做不止一件事的函数是所有罪恶的根源,也是代码中可能遇到的最糟糕的事情之一(嵌套的if语句也是)。它们很混乱,搞得代码难以理解。下面是一个来自实际应用程序的复杂函数示例:

  1. function startProgram() { 
  2.   if (!window.indexedDB) { 
  3.     window.alert("Browser not support indexeDB"); 
  4.   } else { 
  5.     let openRequest = indexedDB.open("store", 1); 
  6.   
  7.     openRequest.onupgradeneeded = () => {}; 
  8.   
  9.     openRequest.onerror = () => { 
  10.       console.error("Error", openRequest.error); 
  11.     }; 
  12.   
  13.     openRequest.onsuccess = () => { 
  14.       let db = openRequest.result; 
  15.     }; 
  16.   
  17.     document.getElementById('stat-op').addEventListener('click', () => {}); 
  18.     document.getElementById('pre2456').addEventListener('click', () => {}); 
  19.     document.getElementById('cpTagList100').addEventListener('change', () => {}); 
  20.     document.getElementById('cpTagList101').addEventListener('click', () => {}); 
  21.     document.getElementById('gototop').addEventListener('click', () => {}); 
  22.     document.getElementById('asp10').addEventListener('click', () => {}); 
  23.   
  24.     fetch("employeList.json") 
  25.       .then(res => res.json()) 
  26.       .then(employes => { 
  27.         document.getElementById("employesSelect").innerHTML = employes.reduce( 
  28.           (content, employe) => employe.name + "<br>", 
  29.           "" 
  30.         ); 
  31.       }); 
  32.   
  33.     document.getElementById("usernameLoged").innerHTML = `Welcome, ${username}`; 
  34.   } 

又多又复杂又让人难以理解的函数

小贴士:由于本例不需要事件侦听器的处理程序,所以删除了它们。

如你所见,这让人困惑,也很难理解里面发生了什么。如果有错误出现,都很难找到并修复它们。如何改进startProgram函数?可以将公共逻辑提取到函数中。以下是具体操作方法:

  1. function startProgram() { 
  2.   if (!window.indexedDB) { 
  3.     throw new Error("Browser doesn't support indexedDB"); 
  4.   } 
  5.  
  6.   initDatabase(); 
  7.   setListeners(); 
  8.   printEmployeeList(); 
  9.  
  10. function initDatabase() { 
  11.   let openRequest = indexedDB.open('store', 1); 
  12.  
  13.   openRequest.onerror = () => { 
  14.     console.error('Error', openRequest.error); 
  15.   }; 
  16.   openRequest.onsuccess = () => { 
  17.     let db = openRequest.result; 
  18.   }; 
  19.  
  20. function setListeners() { 
  21.   document.getElementById('stat-op').addEventListener('click', () => {}); 
  22.   document.getElementById('pre2456').addEventListener('click', () => {}); 
  23.   document.getElementById('cpTagList100').addEventListener('change', () => {}); 
  24.   document.getElementById('cpTagList101').addEventListener('click', () => {}); 
  25.   document.getElementById('gototop').addEventListener('click', () => {}); 
  26.   document.getElementById('asp10').addEventListener('click', () => {}); 
  27.  
  28. async function printEmployeeList() { 
  29.   const employees = await getEmployeeList(); 
  30.  
  31.   document.getElementById('employeeSelect').innerHTML = formatEmployeeList(employees); 
  32.  
  33. function formatEmployeeList(employees) { 
  34.   return employees.reduce( 
  35.     (content, employee) => content + employee.name + '<br>', 
  36.     '' 
  37.   ); 
  38.  
  39. function getEmployeeList() { 
  40.   return fetch('employeeList.json').then(res => res.json()); 

把逻辑提取到函数里

仔细看看startProgram函数的变化:

  • 首先,通过使用一个卫语句替换掉if-else语句。然后,启动数据库所需的逻辑提取到initDatabase函数中,并将事件侦听器添加到setListeners函数中。
  • 打印员工列表的逻辑稍微复杂一些,因此创建了三个函数:printEmployeeList, formatEmployeeList和getEmployeeList。
  • getEmployeeList负责向employeeList.json发出GET请求并以json格式返回响应。
  • 然后由printEmployeeList函数调用getEmployeeList,该函数获取员工列表并将其传递给formatEmployeeList函数,formatEmployeeList函数格式化并返回该列表。然后输出列表。

如你所见,每个功能只负责做一件事。

我们仍然可以对函数进行一些修改,其实,应用程序很需要把视图从控制器中分离出来,但总体而言,startprogram函数现在信息很容易懂,理解它的功能绝对没有困难。如果几个月后必须重新用这段代码,那也不是什么难事。

小结

程序员是唯一负责编写高质量代码的人。我们都应该养成从第一行就写好代码的习惯。编写清晰易懂的代码并不难,这样做对你和你的同事都有好处。

 

责任编辑:赵宁宁 来源: 读芯术
相关推荐

2012-09-03 14:34:39

Java编程代码

2012-05-30 15:58:39

Java编程代码

2009-10-13 14:53:00

2022-09-27 15:34:05

VSCode插件开发

2021-05-19 08:55:37

代码程序员经验分享

2010-12-01 09:15:35

基础架构

2023-06-19 15:36:30

JavaScrip技巧开发

2023-04-26 13:55:00

Python开发技能

2019-07-31 10:24:16

JavaScript浏览器口袋妖怪

2009-04-02 10:59:57

优化插入MySQL

2009-08-18 09:23:58

2021-01-08 07:38:15

代码功能调用

2021-04-16 08:11:07

程序体积优化

2011-08-16 09:47:58

编程

2012-08-03 10:15:10

windows 7操作系统

2021-01-04 05:46:08

代码编程重构

2023-11-05 19:46:56

JavaIntelliJ代码

2021-05-09 09:57:26

MySQL数据库索引

2020-05-08 19:52:31

Reactreact.js前端

2020-06-10 07:49:56

Python代码开发工具
点赞
收藏

51CTO技术栈公众号