重構的藝術:五個小妙招助你寫出好代碼

全文共8912字,預計學習時長14分鐘javascript

做者lloorraa,來源need.pixjava

糟糕的代碼能夠運做,但遲早會讓咱們付出代價。你有沒有遇到過這樣的問題:幾周後,你沒法理解本身的代碼,因而不得不花上幾個小時,甚至幾天的時間來弄清楚到底發生了什麼。程序員

解決這個常見問題的方法是使代碼儘量清晰。若是作得更好的話,即便是非技術人員也應該能理解你的代碼。es6

是時候中止尋找藉口,提升咱們的代碼質量了!golang

編寫清晰的代碼並無那麼複雜。本教程將向你展現五種改進代碼的簡單技巧,並提供一些實例:數據庫

1. 不用switch語句json

咱們一般使用switch語句來代替大型if-else-if語句。可是,switch語句很是冗長,很難維護,甚至很難調試。這些switch語句把咱們的代碼弄得亂七八糟,並且這些語句的語法很奇怪,很不舒服。在添加更多的case時,咱們不得沒必要須手動添加每一個case和break語句,而這就很容易出錯。數組

接下來看一個switch語句的例子:微信

function getPokemon(type) {
async

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函數如今就變得很容易理解。爲何?由於代碼表意更清晰。它可以告訴咱們發生了什麼,這是咱們應該一直努力的方向。

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函數變得能夠從新利用。

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函數如今信息很容易懂,理解它的功能絕對沒有困難。若是幾個月後必須從新用這段代碼,那也不是什麼難事。

小結

程序員是惟一負責編寫高質量代碼的人。咱們都應該養成從第一行就寫好代碼的習慣。編寫清晰易懂的代碼並不難,這樣作對你和你的同事都有好處。

推薦閱讀專題

留言 點贊 關注

咱們一塊兒分享AI學習與發展的乾貨
歡迎關注全平臺AI垂類自媒體 「讀芯術」


(添加小編微信:dxsxbb,加入讀者圈,一塊兒討論最新鮮的人工智能科技哦~)

相關文章
相關標籤/搜索