重構(gòu)的藝術(shù):五個(gè)小妙招助你寫(xiě)出好代碼!
糟糕的代碼可以運(yùn)作,但早晚會(huì)讓我們付出代價(jià)。你有沒(méi)有遇到過(guò)這樣的問(wèn)題:幾周后,你無(wú)法理解自己的代碼,于是不得不花上幾個(gè)小時(shí),甚至幾天的時(shí)間來(lái)弄清楚到底發(fā)生了什么。
解決這個(gè)常見(jiàn)問(wèn)題的方法是使代碼盡可能清晰。如果做得更好的話,即使是非技術(shù)人員也應(yīng)該能理解你的代碼。
是時(shí)候停止尋找借口,提高我們的代碼質(zhì)量了!
編寫(xiě)清晰的代碼并沒(méi)有那么復(fù)雜。本教程將向你展示五種改進(jìn)代碼的簡(jiǎn)單技巧,并提供一些實(shí)例:
1. 不用switch語(yǔ)句
我們通常使用switch語(yǔ)句來(lái)代替大型if-else-if語(yǔ)句。但是,switch語(yǔ)句非常冗長(zhǎng),很難維護(hù),甚至很難調(diào)試。這些switch語(yǔ)句把我們的代碼弄得亂七八糟,而且這些語(yǔ)句的語(yǔ)法很奇怪,很不舒服。在添加更多的case時(shí),我們不得不必須手動(dòng)添加每個(gè)case和break語(yǔ)句,而這就很容易出錯(cuò)。
接下來(lái)看一個(gè)switch語(yǔ)句的例子:
- 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語(yǔ)句
如果需要在switch語(yǔ)句中添加更多的case的話,需要編寫(xiě)的代碼量是相當(dāng)大的。我們可能最終會(huì)復(fù)制粘貼代碼,而其實(shí)我們都知道這種行為的后果是什么。
那么,如何避免使用switch語(yǔ)句呢?可以通過(guò)使用對(duì)象文本。對(duì)象文本簡(jiǎn)單,易于編寫(xiě),方便讀取,維護(hù)輕松。我們都習(xí)慣用javascript處理對(duì)象,對(duì)象文本語(yǔ)法比switch語(yǔ)句更新鮮。下面舉個(gè)例子:
- 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
使用對(duì)象文本替代switch
如你所見(jiàn),可以使用運(yùn)算符 || 添加默認(rèn)值。如果在pokemon對(duì)象中找不到type,getpokemon函數(shù)將使mew返回為默認(rèn)值。
小貼士:你可能已經(jīng)注意到,我們?cè)诤瘮?shù)外部而不是內(nèi)部聲明pokemon對(duì)象。這樣做是為了避免每次執(zhí)行函數(shù)時(shí)都重新創(chuàng)建pokemon。
用映射也能達(dá)到同樣的效果。映射就像對(duì)象一樣是鍵-值對(duì)的集合。不同的是映射允許任何類型的鍵,而對(duì)象只允許字符串作為鍵。此外,映射還有一系列有趣的屬性和方法。
以下是使用映射的方法:
- 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語(yǔ)句
如你所見(jiàn),當(dāng)用對(duì)象文本或映射替換switch語(yǔ)句時(shí),代碼看起來(lái)更清楚、更直接。
2. 把條件語(yǔ)句寫(xiě)的更有描述性
在編寫(xiě)代碼時(shí),條件語(yǔ)句是絕對(duì)必要的。然而,他們很快就會(huì)失控,最終讓我們無(wú)法理解這些語(yǔ)句。這導(dǎo)致我們要么必須編寫(xiě)注釋來(lái)解釋語(yǔ)句的作用,要么必須花費(fèi)寶貴的時(shí)間來(lái)一條一條檢查代碼來(lái)了解發(fā)生了什么。這很糟糕。
看一下下面的語(yǔ)句:
- function checkGameStatus() {
- if (
- remaining === 0 ||
- (remaining === 1 && remainingPlayers === 1) ||
- remainingPlayers === 0
- ) {
- quitGame();
- }
- }
復(fù)雜的條件語(yǔ)句
如果只看前面函數(shù)里if語(yǔ)句中的代碼,很難理解發(fā)生了什么。代碼表意不清楚,不清楚的代碼只會(huì)導(dǎo)致技術(shù)錯(cuò)誤,還會(huì)讓人們非常頭痛。
怎樣改善條件語(yǔ)句呢?可以把它寫(xiě)成一個(gè)函數(shù)。以下是具體操作方法:
- function isGameLost() {
- return (
- remaining === 0 ||
- (remaining === 1 && remainingPlayers === 1) ||
- remainingPlayers === 0
- );
- }
- // Our function is now much easier to understand:
- function checkGameStatus() {
- if (isGameLost()) {
- quitGame();
- }
- }
把條件語(yǔ)句寫(xiě)成函數(shù)
通過(guò)將條件提取到具有描述性名稱isGameLost()的函數(shù)中,checkGameStatus函數(shù)現(xiàn)在就變得很容易理解。為什么?因?yàn)榇a表意更清晰。它能夠告訴我們發(fā)生了什么,這是我們應(yīng)該一直努力的方向。
3. 用衛(wèi)語(yǔ)句(Guard Clauses)代替嵌套的if語(yǔ)句
嵌套if語(yǔ)句是在代碼中可能遇到的最可怕的事情之一。你要是想能夠完全掌握代碼的情況,這絕對(duì)會(huì)讓你精疲力竭。下面是一個(gè)嵌套if語(yǔ)句的例子(這個(gè)嵌套有三層):
- 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語(yǔ)句
你可能需要花幾分鐘時(shí)間上下閱讀,才能了解函數(shù)運(yùn)作的流程。嵌套的if語(yǔ)句很難讓人閱讀和理解。那么,如何才能擺脫討厭的嵌套if語(yǔ)句呢?可以反向思考,使用衛(wèi)語(yǔ)句來(lái)替代這些句子。
“在計(jì)算機(jī)程序設(shè)計(jì)中,衛(wèi)語(yǔ)句是一個(gè)布爾表達(dá)式,如果程序要在有問(wèn)題的分支里繼續(xù)運(yùn)行的話,它的求值必須為true。”——維基百科
通過(guò)顛倒函數(shù)的邏輯,并在函數(shù)開(kāi)始時(shí)放置導(dǎo)致早期退出的條件,它們將變?yōu)楸Wo(hù)程序,并且只允許函數(shù)在滿足所有條件時(shí)繼續(xù)執(zhí)行。這樣可以避免else語(yǔ)句。下面是如何重構(gòu)之前的函數(shù)以使用衛(wèi)語(yǔ)句的方法:
- 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();
- }
用衛(wèi)語(yǔ)句重構(gòu)函數(shù)
如你所見(jiàn),代碼更清晰,更容易理解。我們可以簡(jiǎn)單向下閱讀來(lái)了解函數(shù)的作用,遵循函數(shù)的自然流動(dòng),不像以前那樣上下閱讀。
4. 不要寫(xiě)重復(fù)的代碼
寫(xiě)重復(fù)的代碼總是以失敗告終。它會(huì)導(dǎo)致如下情況:“我在這里修復(fù)了這個(gè)bug,但是忘記在那里修復(fù)”或“我需要在五個(gè)不同的地方更改/添加一個(gè)新功能”。
正如DRY(Don’t repeat yourself不要重復(fù))原則所說(shuō):
每一部分知識(shí)或邏輯都必須在一個(gè)系統(tǒng)中有單一的、明確的表示。 |
因此,代碼越少越好:它節(jié)省了時(shí)間和精力,更易于維護(hù),并減少了錯(cuò)誤的出現(xiàn)。
那么,如何避免重復(fù)代碼呢?這有點(diǎn)難,但是將邏輯提取到函數(shù)/變量通常效果很好。讓我們看看下面的代碼,我在重構(gòu)應(yīng)用程序時(shí)看到了這些代碼:
- 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;
- }
重復(fù)代碼示例
你可能已經(jīng)注意到for循環(huán)在這兩個(gè)函數(shù)中完全相同,除了一個(gè)小細(xì)節(jié):我們想要的新聞?lì)愋停磈avascript或rust新聞。為了避免這種重復(fù),可以將for循環(huán)提取到一個(gè)函數(shù)中,然后從getJavascriptNews,getRustNews和getGolangNews 函數(shù)調(diào)用該函數(shù)。以下是具體操作方法:
- 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循環(huán)提取到getNewsContent函數(shù)中之后,getJavaScriptNews, getRustNews和getGolangNews函數(shù)變成了簡(jiǎn)單、清晰的程序。
(1) 進(jìn)一步重構(gòu)
但是,你是否意識(shí)到,除了傳遞給getNewsContent的類型字符串之外,這兩個(gè)函數(shù)完全相同?這是重構(gòu)代碼時(shí)經(jīng)常發(fā)生的事情。通常情況下,更改一個(gè)會(huì)導(dǎo)致另一個(gè)更改,以此類推,直到重構(gòu)后的代碼最終變成原始代碼的一半大小。代碼告訴你它需要什么:
- 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函數(shù)去了哪里?將它們替換為getNews函數(shù),該函數(shù)將新聞?lì)愋妥鳛閰?shù)接收。這樣,無(wú)論添加多少類型的新聞,總是使用相同的功能。這稱為抽象,允許我們重用函數(shù),因此非常有用。抽象是我在寫(xiě)代碼的時(shí)候最常用的技術(shù)之一。
(2) 補(bǔ)充:使用es6特性使for循環(huán)更具可讀性
for循環(huán)并不完全可讀。通過(guò)引入es6數(shù)組函數(shù),可以有95%的機(jī)會(huì)避免使用它們。在本例中可以使用array.filter和array.map組合來(lái)替換原始循環(huán):
- 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 來(lái)代替循環(huán)
- 用Array.filter只返回其類型等于作為參數(shù)傳遞的類型的元素。
- 用Array.map只返回item對(duì)象的content屬性,而不是整個(gè)item。
恭喜你,經(jīng)過(guò)三次簡(jiǎn)單的重構(gòu),最初的三個(gè)函數(shù)已經(jīng)縮減為兩個(gè),這更容易理解和維護(hù)。另外,抽象讓getNews函數(shù)變得可以重新利用。
5. 一個(gè)函數(shù)只用來(lái)做一件事
一個(gè)函數(shù)應(yīng)該只做一件事。做不止一件事的函數(shù)是所有罪惡的根源,也是代碼中可能遇到的最糟糕的事情之一(嵌套的if語(yǔ)句也是)。它們很混亂,搞得代碼難以理解。下面是一個(gè)來(lái)自實(shí)際應(yīng)用程序的復(fù)雜函數(shù)示例:
- 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}`;
- }
- }
又多又復(fù)雜又讓人難以理解的函數(shù)
小貼士:由于本例不需要事件偵聽(tīng)器的處理程序,所以刪除了它們。
如你所見(jiàn),這讓人困惑,也很難理解里面發(fā)生了什么。如果有錯(cuò)誤出現(xiàn),都很難找到并修復(fù)它們。如何改進(jìn)startProgram函數(shù)?可以將公共邏輯提取到函數(shù)中。以下是具體操作方法:
- 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());
- }
把邏輯提取到函數(shù)里
仔細(xì)看看startProgram函數(shù)的變化:
- 首先,通過(guò)使用一個(gè)衛(wèi)語(yǔ)句替換掉if-else語(yǔ)句。然后,啟動(dòng)數(shù)據(jù)庫(kù)所需的邏輯提取到initDatabase函數(shù)中,并將事件偵聽(tīng)器添加到setListeners函數(shù)中。
- 打印員工列表的邏輯稍微復(fù)雜一些,因此創(chuàng)建了三個(gè)函數(shù):printEmployeeList, formatEmployeeList和getEmployeeList。
- getEmployeeList負(fù)責(zé)向employeeList.json發(fā)出GET請(qǐng)求并以json格式返回響應(yīng)。
- 然后由printEmployeeList函數(shù)調(diào)用getEmployeeList,該函數(shù)獲取員工列表并將其傳遞給formatEmployeeList函數(shù),formatEmployeeList函數(shù)格式化并返回該列表。然后輸出列表。
如你所見(jiàn),每個(gè)功能只負(fù)責(zé)做一件事。
我們?nèi)匀豢梢詫?duì)函數(shù)進(jìn)行一些修改,其實(shí),應(yīng)用程序很需要把視圖從控制器中分離出來(lái),但總體而言,startprogram函數(shù)現(xiàn)在信息很容易懂,理解它的功能絕對(duì)沒(méi)有困難。如果幾個(gè)月后必須重新用這段代碼,那也不是什么難事。
小結(jié)
程序員是唯一負(fù)責(zé)編寫(xiě)高質(zhì)量代碼的人。我們都應(yīng)該養(yǎng)成從第一行就寫(xiě)好代碼的習(xí)慣。編寫(xiě)清晰易懂的代碼并不難,這樣做對(duì)你和你的同事都有好處。