作者 | Jonathan Fulton
譯者 | 彎月,責(zé)編 | 屠敏
頭圖 | CSDN 下載自東方 IC
出品 | CSDN(ID:CSDNnews)
以下為譯文:
幾年前,我們?cè)龅竭^重大的代碼質(zhì)量問題:大多數(shù)文件中的邏輯糾纏夾雜、大量重復(fù)、沒有測試。無論是編寫新功能還是修復(fù)很小的bug都需要付出嘔心瀝血的代價(jià),常常氣到你吐血。令我們苦不堪言。
如今,我們的代碼庫的整體質(zhì)量明顯提高了,這在很大程度上要?dú)w功于我們?yōu)樘岣叽a質(zhì)量而做出的不懈努力。幾年前,在發(fā)現(xiàn)代碼質(zhì)量問題后,我們整個(gè)團(tuán)隊(duì)一起閱讀了Robert Martin的《代碼整潔之道》,然后竭盡全力貫徹了他的建議,甚至引入了“清潔規(guī)范”作為工程團(tuán)隊(duì)的核心文化。如果你打算擴(kuò)張團(tuán)隊(duì),那么我強(qiáng)烈建議你現(xiàn)在就開始實(shí)施這兩項(xiàng)措施。從長遠(yuǎn)來看,恰當(dāng)?shù)貙?shí)施“干凈的代碼”實(shí)踐可以提高一倍生產(chǎn)力,并顯著提高工程團(tuán)隊(duì)的士氣。有了選擇,誰還會(huì)愿意進(jìn)入上圖右邊那個(gè)Bad code的房間呢?
在我們實(shí)施的“清潔規(guī)范”以及其他想法之中,有四項(xiàng)措施將團(tuán)隊(duì)的生產(chǎn)力和幸福指數(shù)提高了80%。
-
沒有經(jīng)過測試的代碼一概不安全。
你需要編寫大量測試,尤其是單元測試,否則你會(huì)追悔莫及。
-
選擇有意義的名稱。
為變量、類和函數(shù)選擇言簡意賅的名稱。
-
類與函數(shù)保持最小,遵守單一功能原則
函數(shù)不應(yīng)超過4行,而類不應(yīng)超過100行。是的,你沒看錯(cuò)。而且它們應(yīng)該只做一件事。
-
函數(shù)不能有副作用
副作用(例如,修改輸入?yún)?shù))是有害的。請(qǐng)確保你的代碼中沒有副作用。盡可能在函數(shù)聲明中明確規(guī)定這一點(diǎn)(例如,傳入基本類型,或者傳遞沒有setter的對(duì)象)。
下面我們來詳細(xì)介紹上述每一點(diǎn),幫助你理解并應(yīng)用到工程團(tuán)隊(duì)的日常工作中。
沒有經(jīng)過測試的代碼一概不安全
每當(dāng)遇到原本應(yīng)該在測試捕捉到的bug時(shí),我就會(huì)對(duì)我們的工程師重復(fù)這句話。除非你建立了測試的文化,否則你也會(huì)一次又一次地引用這句話。你需要編寫大量測試,尤其是單元測試。認(rèn)真考慮集成測試,并確保你的測試用例足夠涵蓋核心業(yè)務(wù)功能。請(qǐng)記住,如果有一段代碼沒有被測試覆蓋到,那么將來肯定會(huì)出問題,而且你根本意識(shí)不到,直到被你的客戶發(fā)現(xiàn)。
你需要一遍又一遍地向團(tuán)隊(duì)成員重復(fù)這句話:“沒有經(jīng)過測試的代碼一概不安全”,直到這句話在每個(gè)人的心里生根。無論你是剛畢業(yè)的新手軟件工程師還是經(jīng)驗(yàn)豐富的資深軟件工程師,都應(yīng)該時(shí)刻履行這個(gè)實(shí)踐。
選擇有意義的名稱
計(jì)算機(jī)科學(xué)界有兩大難題:緩存失效和命名。
可能你曾聽過這句話,這與工程團(tuán)隊(duì)的日常工作有著莫大的關(guān)系。如果你和你的團(tuán)隊(duì)成員不擅長代碼中的命名,那么你們的維護(hù)工作就會(huì)變成一場噩夢(mèng),而且你將一事無成。你會(huì)失去最優(yōu)秀的開發(fā)人員,而且你的公司距離倒閉也不遠(yuǎn)了。
這個(gè)問題非常嚴(yán)重,你不應(yīng)該使用諸如data、foobar或myNumber之類不恰當(dāng)?shù)淖兞棵乙步^對(duì)不能將SomethingManager作為類名稱。務(wù)必使用言簡意賅的名稱,確保在發(fā)生沖突時(shí)能準(zhǔn)確找到。良好的命名不僅可以大幅提高開發(fā)人員的效率,而且還可以通過IDE的快捷方式“按名稱查找” 等輕松查找文件。另外,良好的命名需要通過嚴(yán)格的代碼審核貫徹。
類與函數(shù)保持最小,遵守單一功能原則
這兩大原則的關(guān)系就像雞和雞蛋一樣,無論先有雞還是先有蛋,有了二者的因果輪回,才有我們無盡的美味。下面先來談?wù)勵(lì)惻c函數(shù)保持最小。
“小”對(duì)函數(shù)意味著什么?不超過4行代碼。是的,你沒看錯(cuò),就是4行。你可能現(xiàn)在就想告辭了,但是千萬別走。雖然這個(gè)數(shù)字看起來有些武斷,而且太少,你一輩子可能都沒寫過像這樣的代碼。但是,只有4行代碼的函數(shù)會(huì)強(qiáng)迫你認(rèn)真思考,并為子函數(shù)選擇真正的好名字,而這些子函數(shù)就是代碼最好的文檔。另外,4行代碼意味著你不會(huì)使用嵌套的IF語句,省得你需要耗費(fèi)大量腦力搞清楚所有的代碼路徑。
下面讓我們一起來看一個(gè)例子。Node有一個(gè)名為“build-url”的npm模塊,用途如其名所示:構(gòu)建URL。你可以通過這個(gè)鏈接(https://github.com/steverydz/build-url/blob/master/src/build-url.js)查看源文件。以下是相關(guān)代碼。
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;
};
請(qǐng)注意,此函數(shù)長35行。雖然理解起來也不是非常難,但如果我們應(yīng)用“小”原則來重構(gòu)輔助函數(shù),則會(huì)大大簡化。更新和改進(jìn)后的版本如下。
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}`;
}
你會(huì)注意到,雖然我們沒有嚴(yán)格遵守每個(gè)函數(shù)4行的原則,但我們創(chuàng)建了幾個(gè)相對(duì)“較小”的函數(shù)。每個(gè)函數(shù)僅完成一項(xiàng)任務(wù),你可以根據(jù)函數(shù)名輕松理解這段代碼。如果需要,你甚至可以針對(duì)每個(gè)函數(shù)進(jìn)行單元測試,而不是只測試一個(gè)大型的buildUrl函數(shù)。你可能還會(huì)注意到,這種方法產(chǎn)生的代碼略多一些,從35行變成了55行。這完全可以接受,因?yàn)檫@55行代碼比原來的35行更加方便維護(hù)和閱讀。
如何才能編寫出這樣的代碼?我個(gè)人認(rèn)為,最簡單的方法是列出你希望逐步完成的各項(xiàng)任務(wù)。每一步都可以是建立某個(gè)子函數(shù)或輔助函數(shù)。例如,針對(duì)上述buildUrl函數(shù)我們希望完成如下工作:
-
初始化baseUrl和options
-
添加路徑(如果有的話)
-
添加查詢參數(shù)(如果有的話)
-
添加錨點(diǎn)(如果有的話)
請(qǐng)注意,上述每一步都可以直接轉(zhuǎn)化為子函數(shù)。一旦養(yǎng)成了這樣的習(xí)慣,你就可以使用這種自頂向下的方法編寫所有代碼,然后根據(jù)上述步驟列表,建立子函數(shù),再針對(duì)每個(gè)子函數(shù)繼續(xù)遞歸,創(chuàng)建步驟列表、建立子函數(shù),以此類推。
下面再來談?wù)剢我还δ茉瓌t。根據(jù)維基百科,單一功能原則的定義如下:
在面向?qū)ο缶幊填I(lǐng)域中,單一功能原則(Single Responsibility Principle)規(guī)定每個(gè)類都應(yīng)該有一個(gè)單一的功能,并且該功能應(yīng)該由這個(gè)類完全封裝起來。所有它的(這個(gè)類的)服務(wù)都應(yīng)該嚴(yán)密的和該功能平行(功能平行,意味著沒有依賴)。
在《代碼整潔之道》中,Robert Martin給出了另一個(gè)定義:
單一功能原則表明,類或模塊應(yīng)有且只有一條加以修改的理由。
假設(shè)我們正在建立一個(gè)需要某種報(bào)告以及顯示報(bào)告的系統(tǒng)。比較樸素的做法是構(gòu)建一個(gè)存儲(chǔ)報(bào)告數(shù)據(jù)以及用于顯示報(bào)告的邏輯模塊/類。但是,這違反了單一功能原則,因?yàn)樾薷脑擃惖母邔釉虺霈F(xiàn)了兩個(gè)。首先,如果報(bào)告字段發(fā)生變化,我們需要修改類;其次,如果報(bào)表可視化要求發(fā)生變化,我們也需要修改類。因此,我們不提倡利用一個(gè)類存儲(chǔ)數(shù)據(jù)和顯示數(shù)據(jù)的做法,我們應(yīng)該將這些概念和所有權(quán)區(qū)域劃分為兩個(gè)不同的類,例如ReportData和ReportDataRenderer等。
函數(shù)不能有副作用
副作用確實(shí)是罪惡之源,因?yàn)楦弊饔玫拇嬖冢帉憶]有錯(cuò)誤的代碼會(huì)非常困難。看看下面的例子,你能看出副作用嗎?
functiongetUserByEmailAndPassword(email, password) {
let user = UserService.getByEmailAndPassword(email, password);
if (user) {
LoginService.loginUser(user); //Log user in, add cookie (Side effect!!!!)
}
return user;
}
根據(jù)函數(shù)名所示,這個(gè)函數(shù)的目的是通過電子郵件/密碼組合查找用戶,這是所有Web應(yīng)用程序的標(biāo)準(zhǔn)操作。然而,如果你沒有閱讀代碼的實(shí)現(xiàn),就不知道這個(gè)函數(shù)還有一個(gè)隱藏的副作用:在用戶登錄時(shí),創(chuàng)建一個(gè)登錄令牌,將其添加到數(shù)據(jù)庫中,然后將cookie發(fā)送給用戶,而用戶則“成功登錄”。
這中間有很多問題。
首先,不閱讀實(shí)現(xiàn)代碼就不知道該函數(shù)的功能/接口。即使你通過文檔說明該函數(shù)登錄的副作用,也仍然不是理想的做法。工程師喜歡使用現(xiàn)代IDE中的智能提示,因此當(dāng)遇到一個(gè)簡單的函數(shù)名時(shí),大部分人都不會(huì)閱讀相應(yīng)的文檔。他們會(huì)利用這個(gè)函數(shù)來獲取用戶對(duì)象,卻沒有意識(shí)到他們正在請(qǐng)求中添加Cookie,這可能會(huì)引發(fā)很多棘手且不易發(fā)現(xiàn)的bug。
其次,考慮到所有的依賴關(guān)系,測試這個(gè)函數(shù)相當(dāng)困難。你需要驗(yàn)證是否可以通過電子郵件/密碼順利找到用戶,需要模擬HTTP響應(yīng)以及登錄令牌的寫入。
第三,用戶查找和登錄之間的緊密結(jié)合必然無法滿足將來的所有用例,例如,你可能需要單獨(dú)查找用戶或登錄用戶。換句話說,這個(gè)函數(shù)不具有前瞻性。
總結(jié)
總的來說,你需要牢記以下四個(gè)提高代碼整潔度的原則,并通過這些原則提高團(tuán)隊(duì)的生產(chǎn)力:
-
沒有經(jīng)過測試的代碼一概不安全
-
選擇有意義的名稱
-
類與函數(shù)保持最小,遵守單一功能原則
-
函數(shù)不能有副作用
感謝您的閱讀!
原文:https://engineering.videoblocks.com/these-four-clean-code-tips-will-dramatically-improve-your-engineering-teams-productivity-b5bd121dd150