作者 | Jonathan Fulton
譯者 | 彎月,責編 | 屠敏
頭圖 | CSDN 下載自東方 IC
出品 | CSDN(ID:CSDNnews)
以下為譯文:
幾年前,我們曾遇到過重大的代碼質量問題:大多數文件中的邏輯糾纏夾雜、大量重復、沒有測試。無論是編寫新功能還是修復很小的bug都需要付出嘔心瀝血的代價,常常氣到你吐血。令我們苦不堪言。

如今,我們的代碼庫的整體質量明顯提高了,這在很大程度上要歸功于我們為提高代碼質量而做出的不懈努力。幾年前,在發現代碼質量問題后,我們整個團隊一起閱讀了Robert Martin的《代碼整潔之道》,然后竭盡全力貫徹了他的建議,甚至引入了“清潔規范”作為工程團隊的核心文化。如果你打算擴張團隊,那么我強烈建議你現在就開始實施這兩項措施。從長遠來看,恰當地實施“干凈的代碼”實踐可以提高一倍生產力,并顯著提高工程團隊的士氣。有了選擇,誰還會愿意進入上圖右邊那個Bad code的房間呢?
在我們實施的“清潔規范”以及其他想法之中,有四項措施將團隊的生產力和幸福指數提高了80%。
-
沒有經過測試的代碼一概不安全。
你需要編寫大量測試,尤其是單元測試,否則你會追悔莫及。
-
選擇有意義的名稱。
為變量、類和函數選擇言簡意賅的名稱。
-
類與函數保持最小,遵守單一功能原則
函數不應超過4行,而類不應超過100行。是的,你沒看錯。而且它們應該只做一件事。
-
函數不能有副作用
副作用(例如,修改輸入參數)是有害的。請確保你的代碼中沒有副作用。盡可能在函數聲明中明確規定這一點(例如,傳入基本類型,或者傳遞沒有setter的對象)。
下面我們來詳細介紹上述每一點,幫助你理解并應用到工程團隊的日常工作中。
沒有經過測試的代碼一概不安全
每當遇到原本應該在測試捕捉到的bug時,我就會對我們的工程師重復這句話。除非你建立了測試的文化,否則你也會一次又一次地引用這句話。你需要編寫大量測試,尤其是單元測試。認真考慮集成測試,并確保你的測試用例足夠涵蓋核心業務功能。請記住,如果有一段代碼沒有被測試覆蓋到,那么將來肯定會出問題,而且你根本意識不到,直到被你的客戶發現。
你需要一遍又一遍地向團隊成員重復這句話:“沒有經過測試的代碼一概不安全”,直到這句話在每個人的心里生根。無論你是剛畢業的新手軟件工程師還是經驗豐富的資深軟件工程師,都應該時刻履行這個實踐。
選擇有意義的名稱
計算機科學界有兩大難題:緩存失效和命名。
可能你曾聽過這句話,這與工程團隊的日常工作有著莫大的關系。如果你和你的團隊成員不擅長代碼中的命名,那么你們的維護工作就會變成一場噩夢,而且你將一事無成。你會失去最優秀的開發人員,而且你的公司距離倒閉也不遠了。
這個問題非常嚴重,你不應該使用諸如data、foobar或myNumber之類不恰當的變量名,而且也絕對不能將SomethingManager作為類名稱。務必使用言簡意賅的名稱,確保在發生沖突時能準確找到。良好的命名不僅可以大幅提高開發人員的效率,而且還可以通過IDE的快捷方式“按名稱查找” 等輕松查找文件。另外,良好的命名需要通過嚴格的代碼審核貫徹。
類與函數保持最小,遵守單一功能原則
這兩大原則的關系就像雞和雞蛋一樣,無論先有雞還是先有蛋,有了二者的因果輪回,才有我們無盡的美味。下面先來談談類與函數保持最小。
“小”對函數意味著什么?不超過4行代碼。是的,你沒看錯,就是4行。你可能現在就想告辭了,但是千萬別走。雖然這個數字看起來有些武斷,而且太少,你一輩子可能都沒寫過像這樣的代碼。但是,只有4行代碼的函數會強迫你認真思考,并為子函數選擇真正的好名字,而這些子函數就是代碼最好的文檔。另外,4行代碼意味著你不會使用嵌套的IF語句,省得你需要耗費大量腦力搞清楚所有的代碼路徑。
下面讓我們一起來看一個例子。Node有一個名為“build-url”的npm模塊,用途如其名所示:構建URL。你可以通過這個鏈接(https://github.com/steverydz/build-url/blob/master/src/build-url.js)查看源文件。以下是相關代碼。
function buildUrl(url, options) {
var queryString = ;
var key;
var builtUrl;
if (url === ) {
builtUrl = '';
} else if (typeof(url) === 'object') {
builtUrl = '';
options = url;
} else {
builtUrl = url;
}
if (options) {
if (options.path) {
builtUrl += '/' + options.path;
}
if (options.queryParams) {
for (key in options.queryParams) {
if (options.queryParams.hasOwnProperty(key)) {
queryString.push(key + '=' + options.queryParams[key]);
}
}
builtUrl += '?' + queryString.join('&');
}
if (options.hash) {
builtUrl += '#' + options.hash;
}
}
return builtUrl;
};
請注意,此函數長35行。雖然理解起來也不是非常難,但如果我們應用“小”原則來重構輔助函數,則會大大簡化。更新和改進后的版本如下。
function buildUrl(url, options) {
const baseUrl = _getBaseUrl(url);
const opts = _getOptions(url, options);
if (!opts) {
return baseUrl;
}
urlWithPath = _AppendPath(baseUrl, opts.path);
urlWithPathAndQueryParams = _appendQueryParams(urlWithPath, opts.queryParams)
urlWithPathQueryParamsAndHash = _appendHash(urlWithPathAndQueryParams, opts.hash);
return urlWithPathQueryParamsAndHash;
};
function _getBaseUrl(url) {
if (url === || typeof(url) === 'object') {
return '';
}
return url;
}
function _getOptions(url, options) {
if (typeof(url) === 'object') {
return url;
}
return options;
}
function _appendPath(baseUrl, path) {
if (!path) {
return baseUrl;
}
return baseUrl += '/' + path;
}
function _appendQueryParams(urlWithPath, queryParams) {
if (!queryParams) {
return urlWithPath
}
const keyValueStrings = Object.keys(queryParams).map(key => {
return `${key}=${queryParams[key]}`;
});
const joinedKeyValueStrings = keyValueStrings.join('&');
return `${urlWithPath}?${joinedKeyValueStrings}`;
}
function _appendHash(urlWithPathAndQueryParams, hash) {
if (!hash) {
return urlWithPathAndQueryParams;
}
return `${urlWithPathAndQueryParams}#${hash}`;
}
你會注意到,雖然我們沒有嚴格遵守每個函數4行的原則,但我們創建了幾個相對“較小”的函數。每個函數僅完成一項任務,你可以根據函數名輕松理解這段代碼。如果需要,你甚至可以針對每個函數進行單元測試,而不是只測試一個大型的buildUrl函數。你可能還會注意到,這種方法產生的代碼略多一些,從35行變成了55行。這完全可以接受,因為這55行代碼比原來的35行更加方便維護和閱讀。
如何才能編寫出這樣的代碼?我個人認為,最簡單的方法是列出你希望逐步完成的各項任務。每一步都可以是建立某個子函數或輔助函數。例如,針對上述buildUrl函數我們希望完成如下工作:
-
初始化baseUrl和options
-
添加路徑(如果有的話)
-
添加查詢參數(如果有的話)
-
添加錨點(如果有的話)
請注意,上述每一步都可以直接轉化為子函數。一旦養成了這樣的習慣,你就可以使用這種自頂向下的方法編寫所有代碼,然后根據上述步驟列表,建立子函數,再針對每個子函數繼續遞歸,創建步驟列表、建立子函數,以此類推。
下面再來談談單一功能原則。根據維基百科,單一功能原則的定義如下:
在面向對象編程領域中,單一功能原則(Single Responsibility Principle)規定每個類都應該有一個單一的功能,并且該功能應該由這個類完全封裝起來。所有它的(這個類的)服務都應該嚴密的和該功能平行(功能平行,意味著沒有依賴)。
在《代碼整潔之道》中,Robert Martin給出了另一個定義:
單一功能原則表明,類或模塊應有且只有一條加以修改的理由。
假設我們正在建立一個需要某種報告以及顯示報告的系統。比較樸素的做法是構建一個存儲報告數據以及用于顯示報告的邏輯模塊/類。但是,這違反了單一功能原則,因為修改該類的高層原因出現了兩個。首先,如果報告字段發生變化,我們需要修改類;其次,如果報表可視化要求發生變化,我們也需要修改類。因此,我們不提倡利用一個類存儲數據和顯示數據的做法,我們應該將這些概念和所有權區域劃分為兩個不同的類,例如ReportData和ReportDataRenderer等。
函數不能有副作用
副作用確實是罪惡之源,因為副作用的存在,編寫沒有錯誤的代碼會非常困難。看看下面的例子,你能看出副作用嗎?
functiongetUserByEmailAndPassword(email, password) {
let user = UserService.getByEmailAndPassword(email, password);
if (user) {
LoginService.loginUser(user); //Log user in, add cookie (Side effect!!!!)
}
return user;
}
根據函數名所示,這個函數的目的是通過電子郵件/密碼組合查找用戶,這是所有Web應用程序的標準操作。然而,如果你沒有閱讀代碼的實現,就不知道這個函數還有一個隱藏的副作用:在用戶登錄時,創建一個登錄令牌,將其添加到數據庫中,然后將cookie發送給用戶,而用戶則“成功登錄”。
這中間有很多問題。
首先,不閱讀實現代碼就不知道該函數的功能/接口。即使你通過文檔說明該函數登錄的副作用,也仍然不是理想的做法。工程師喜歡使用現代IDE中的智能提示,因此當遇到一個簡單的函數名時,大部分人都不會閱讀相應的文檔。他們會利用這個函數來獲取用戶對象,卻沒有意識到他們正在請求中添加Cookie,這可能會引發很多棘手且不易發現的bug。
其次,考慮到所有的依賴關系,測試這個函數相當困難。你需要驗證是否可以通過電子郵件/密碼順利找到用戶,需要模擬HTTP響應以及登錄令牌的寫入。
第三,用戶查找和登錄之間的緊密結合必然無法滿足將來的所有用例,例如,你可能需要單獨查找用戶或登錄用戶。換句話說,這個函數不具有前瞻性。
總結
總的來說,你需要牢記以下四個提高代碼整潔度的原則,并通過這些原則提高團隊的生產力:
-
沒有經過測試的代碼一概不安全
-
選擇有意義的名稱
-
類與函數保持最小,遵守單一功能原則
-
函數不能有副作用
感謝您的閱讀!
原文:https://engineering.videoblocks.com/these-four-clean-code-tips-will-dramatically-improve-your-engineering-teams-productivity-b5bd121dd150