33

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI2NjkyNDQ3Mw%3D%3D&%3Bmid=2247491759&%3Bidx=1&%3Bsn=82189cdad2cee95b65393c0c10c2011d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

EjyQZvZ.jpg!web

全文共 8912 字,预计学习时长 14 分钟

VVVFVfI.jpg!web

作者lloorraa,来源need.pix

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

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

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

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

1. 不用switch语句

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

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

function getPokemon(type) {

let pokemon;

switch (type) {

case 'Water':

pokemon = 'Squirtle';

break;

case 'Fire':

pokemon = 'Charmander';

break;

case 'Plant':

pokemon = 'Bulbasur';

break;

case 'Electric':

pokemon = 'Pikachu';

break;

default:

pokemon = 'Mew';

}

return pokemon;

}

console.log(getPokemon('Fire')); // Result: Charmander

Switch语句

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

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

const pokemon = {

Water: 'Squirtle',

Fire: 'Charmander',

Plant: 'Bulbasur',

Electric: 'Pikachu'

};

function getPokemon(type) {

return pokemon[type] || 'Mew';

}

console.log(getPokemon('Fire')); // Result: Charmander

// If the type isn't found in the pokemon object, the function will return the default value 'Mew'

console.log(getPokemon('unknown')); // Result: Mew

使用对象文本替代switch

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

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

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

以下是使用映射的方法:

const pokemon = new Map([

['Water', 'Squirtle'],

['Fire', 'Charmander'],

['Plant', 'Bulbasur'],

['Electric', 'Pikachu']

]);

function getPokemon(type) {

return pokemon.get(type) || 'Mew';

}

console.log(getPokemon('Fire')); // Result: Charmander

console.log(getPokemon('unknown')); // Result: Mew

用映射代替switch语句

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

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

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

看一下下面的语句:

function checkGameStatus() {

if (

remaining === 0 ||

(remaining === 1 && remainingPlayers === 1) ||

remainingPlayers === 0

) {

quitGame();

}

}

复杂的条件语句

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

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

function isGameLost() {

return (

remaining === 0 ||

(remaining === 1 && remainingPlayers === 1) ||

remainingPlayers === 0

);

}

// Our function is now much easier to understand:

function checkGameStatus() {

if (isGameLost()) {

quitGame();

}

}

把条件语句写成函数

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

Vvu6jaZ.jpg!web

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

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

function writeTweet() {

const tweet = writeSomething();

if (isLoggedIn()) {

if (tweet) {

if (isTweetDoubleChecked()) {

tweetIt();

} else {

throw new Error('Dont publish without double checking your tweet');

}

} else {

throw new Error("Your tweet is empty, can't publish it");

}

} else {

throw new Error('You need to log in before tweeting');

}

}

嵌套的if语句

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

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

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

function writeTweet() {

const tweet = writeSomething();

if (!isLoggedIn()) {

throw new Error('You need to log in before tweeting');

}

if (!tweet) {

throw new Error("Your tweet is empty, can't publish it");

}

if (!isTweetDoubleChecked()) {

throw new Error('Dont publish without double checking your tweet');

}

tweetIt();

}

用卫语句重构函数

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

4. 不要写重复的代码

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

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

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

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

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

function getJavascriptNews() {

const allNews = getNewsFromWeb();

const news = [];

for (let i = allNews.length - 1; i >= 0; i--){

if (allNews[i].type === "javascript") {

news.push(allNews[i]);

}

}

return news;

}

function getRustNews() {

const allNews = getNewsFromWeb();

const news = [];

for (let i = allNews.length - 1; i >= 0; i--){

if (allNews[i].type === "rust") {

news.push(allNews[i]);

}

}

return news;

}

function getGolangNews() {

const news = [];

const allNews = getNewsFromWeb();

for (let i = allNews.length - 1; i >= 0; i--) {

if (allNews[i].type === 'golang') {

news.push(allNews[i]);

}

}

return news;

}

重复代码示例

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

function getJavascriptNews() {

const allNews = getNewsFromWeb();

return getNewsContent(allNews, 'javascript');

}

function getRustNews() {

const allNews = getNewsFromWeb();

return getNewsContent(allNews, 'rust');

}

function getGolangNews() {

const allNews = getNewsFromWeb();

return getNewsContent(allNews, 'golang');

}

function getNewsContent(newsList, type) {

const news = [];

for (let i = newsList.length - 1; i >= 0; i--) {

if (newsList[i].type === type) {

news.push(newsList[i].content);

}

}

return news;

}

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

进一步重构

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

function getNews(type) {

const allNews = getNewsFromWeb();

return getNewsContent(allNews, type);

}

function getNewsContent(newsList, type) {

const news = [];

for (let i = newsList.length - 1; i >= 0; i--) {

if (newsList[i].type === type) {

news.push(newsList[i].content);

}

}

return news;

}

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

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

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

function getNews(type) {

const allNews = getNewsFromWeb();

return getNewsContent(allNews, type);

}

function getNewsContent(newsList, type) {

return newsList

.filter(newsItem => newsItem.type === type)

.map(newsItem => newsItem.content);

}

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

• 用Array.filter只返回其类型等于作为参数传递的类型的元素。

• 用Array.map只返回item对象的content属性,而不是整个item。

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

EBFrie2.jpg!web

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

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

function startProgram() {

if (!window.indexedDB) {

window.alert("Browser not support indexeDB");

} else {

let openRequest = indexedDB.open("store", 1);

openRequest.onupgradeneeded = () => {};

openRequest.onerror = () => {

console.error("Error", openRequest.error);

};

openRequest.onsuccess = () => {

let db = openRequest.result;

};

document.getElementById('stat-op').addEventListener('click', () => {});

document.getElementById('pre2456').addEventListener('click', () => {});

document.getElementById('cpTagList100').addEventListener('change', () => {});

document.getElementById('cpTagList101').addEventListener('click', () => {});

document.getElementById('gototop').addEventListener('click', () => {});

document.getElementById('asp10').addEventListener('click', () => {});

fetch("employeList.json")

.then(res => res.json())

.then(employes => {

document.getElementById("employesSelect").innerHTML = employes.reduce(

(content, employe) => employe.name + "<br>",

""

);

});

document.getElementById("usernameLoged").innerHTML = `Welcome, ${username}`;

}

}

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

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

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

function startProgram() {

if (!window.indexedDB) {

throw new Error("Browser doesn't support indexedDB");

}

initDatabase();

setListeners();

printEmployeeList();

}

function initDatabase() {

let openRequest = indexedDB.open('store', 1);

openRequest.onerror = () => {

console.error('Error', openRequest.error);

};

openRequest.onsuccess = () => {

let db = openRequest.result;

};

}

function setListeners() {

document.getElementById('stat-op').addEventListener('click', () => {});

document.getElementById('pre2456').addEventListener('click', () => {});

document.getElementById('cpTagList100').addEventListener('change', () => {});

document.getElementById('cpTagList101').addEventListener('click', () => {});

document.getElementById('gototop').addEventListener('click', () => {});

document.getElementById('asp10').addEventListener('click', () => {});

}

async function printEmployeeList() {

const employees = await getEmployeeList();

document.getElementById('employeeSelect').innerHTML = formatEmployeeList(employees);

}

function formatEmployeeList(employees) {

return employees.reduce(

(content, employee) => content + employee.name + '<br>',

''

);

}

function getEmployeeList() {

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函数现在信息很容易懂,理解它的功能绝对没有困难。如果几个月后必须重新用这段代码,那也不是什么难事。

小结

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

UZbAzej.jpg!web

推荐阅读专题

mMby2iU.jpg!web

iYjIniQ.jpg!web

IjUZ322.jpg!web

jmyiMne.jpg!web

Nbqmy26.jpg!web

留言 点赞 发个朋友圈

我们一起分享AI学习与发展的干货

编译组:林芳如、龚雪

相关链接:

https://medium.com/better-programming/the-art-of-refactoring-5-tips-to-write-better-code-3bc1f6f7689 

如需转载,请后台留言,遵守转载规范

推荐文章阅读

ACL2018论文集50篇解读

EMNLP2017论文集28篇论文解读

2018年AI三大顶会中国学术成果全链接

ACL2017 论文集:34篇解读干货全在这里

10篇AAAI2017经典论文回顾

长按识别二维码可添加关注

读芯君爱你

2ABbUry.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK