作者:llinvokerl
看完《代碼整潔之道 》之后我受益匪淺,但等到自己實(shí)踐時(shí)卻很難按照書中給的建議編寫出整潔的代碼。一方面是規(guī)則太多,記不住,另一方面書上引用了大量示例代碼對這些規(guī)則進(jìn)行佐證,在我記不住時(shí)亦不方便查閱。于是我把書中的規(guī)則摘了出來并加以一定的解釋,閑暇時(shí)候多過幾遍,希望這些規(guī)則時(shí)刻警示我,成為我的習(xí)慣。
想看此書卻還沒開始的人也可以從這篇筆記出發(fā),對筆記中列出的規(guī)則有疑問再翻書找答案,相信會比直接啃書來的快一些。
ps: 未必要嚴(yán)格遵循書中的規(guī)則,代碼不是八股文。
命名 1.避免誤導(dǎo) 2.做有意義的區(qū)分 不以數(shù)字系列命名 (a1、a2、a3),按照真實(shí)含義命名
Product/ProductInfo/ProductData 意思無區(qū)別,只統(tǒng)一用一個(gè)
別寫冗余的名字,變量名別帶variable 、表名別帶table
3.使用可搜索的名稱 4.命名時(shí)避免使用編碼 把類型和作用域編碼進(jìn)名稱里增加了解碼負(fù)擔(dān)。意味著新人除了了解代碼邏輯之外,還需要學(xué)習(xí)這種編碼語言 。
別使用匈牙利語標(biāo)記法 (格式:[Prefix]-BaseTag-Name 其中BaseTag是數(shù)據(jù)類型的縮寫,Name是變量名字),純屬多余
不必用'm_'前綴來表明成員變量
接口和實(shí)現(xiàn)別在名稱中編碼。接口名IShapeFactory 的前導(dǎo)'I'是廢話 。如果接口和實(shí)現(xiàn)必須選一個(gè)編碼,寧可選實(shí)現(xiàn),ShapeFactoryImp 都比對接口名稱編碼來的好
5.類名、方法名 6.每個(gè)概念用一個(gè)詞 7.別用雙關(guān)語 8.添加有意義的語境 函數(shù) 1.越短小越好 2.只做一件事 3.每個(gè)函數(shù)一個(gè)抽象層級 4.switch語句 5.使用描述性的名稱 6.函數(shù)參數(shù) 參數(shù)越少越好 ,0參數(shù)最好,盡量避免用三個(gè)以上參數(shù)
參數(shù)越多,編寫組合參數(shù)的測試用例就越困難
別用標(biāo)識參數(shù) ,向函數(shù)傳入bool 值是不好的,這意味著函數(shù)不止做一件事。可以將此函數(shù)拆成兩個(gè)。
如果函數(shù)需要兩個(gè)、三個(gè)或者三個(gè)以上參數(shù) ,就說明其中一些參數(shù)應(yīng)該封裝成類了 。
將參數(shù)的順序編碼進(jìn)函數(shù)名 ,減輕記憶參數(shù)順序的負(fù)擔(dān),例如,assertExpectedEqualsActual(expected, actual)
7.副作用(函數(shù)在正常工作任務(wù)之外對外部環(huán)境所施加的影響) 8.設(shè)置(寫)和查詢(讀)分離 if(set('username', 'unclebob')) { ... } 的含義模糊不清。應(yīng)該改為:
if (attributeExists('username')) { setAttribute('username', 'unclebob'); ... }
9.使用異常代替返回錯(cuò)誤碼 返回錯(cuò)誤碼 會要求調(diào)用者立刻處理錯(cuò)誤,從而引起深層次的嵌套結(jié)構(gòu) :
if (deletePate(page) == E_OK) { if (xxx() == E_OK) { if (yyy() == E_OK) { log(); } else { log(); } } else { log(); } } else { log(); }使用異常機(jī)制:
try { deletePage(); xxx(); yyy(); } catch (Exception e) { log(e->getMessage());
- try/catch代碼塊丑陋不堪,所以**最好把try和catch代碼塊的主體抽離出來,單獨(dú)形成函數(shù)** try { do(); } catch (Exception e) { handle(); }函數(shù)只做一件事,錯(cuò)誤處理就是一件事。如果關(guān)鍵字try在某個(gè)函數(shù)中存在,它就該是函數(shù)的第一個(gè)單詞,而且catch代碼塊后面也不該有其他內(nèi)容。
定義錯(cuò)誤碼的class需要添加新的錯(cuò)誤碼時(shí),所有用到錯(cuò)誤碼class的其他class都得重新編譯和部署 。但如果使用異常而非錯(cuò)誤碼,新異??梢詮漠惓lass派生出來,無需重新編譯或部署 。這也是開放閉合原則(對擴(kuò)展開放,對修改封閉)的范例。
10.不要寫重復(fù)代碼 11.結(jié)構(gòu)化編程 12.如何寫出這樣的函數(shù) 注釋 1.用代碼來闡述 創(chuàng)建一個(gè)與注釋所言同一事物的函數(shù)即可
// check to see if the employee is eligible for full benefits if ((employee.falgs & HOURLY_FLAG) && (employee.age > 65))
應(yīng)替換為
if (employee.isEligibleForFullBenefits())2.好注釋 法律信息
提供基本信息,如解釋某個(gè)抽象方法的返回值
對意圖的解釋,反應(yīng)了作者某個(gè)決定后面的意圖
闡釋。把某些晦澀的參數(shù)或者返回值 的意義翻譯成可讀的形式 (更好的方法是讓它們自身變得足夠清晰,但是類似標(biāo)準(zhǔn)庫的代碼我們無法修改):
if (b.compareTo(a) == 1) //b > a
警示。// don't run unless you have some time to kill
TODO 注釋
放大 一些看似不合理之物 的重要性
3.壞注釋 自言自語
多余的注釋。把邏輯在注釋里寫一遍不能比代碼提供更多信息 ,讀它不比讀代碼簡單。一目了然的成員變量別加注釋 ,顯得很多余。
誤導(dǎo)性注釋
遵循規(guī)矩的注釋。每個(gè)函數(shù)都加注釋、每個(gè)變量都加注釋是愚蠢的 。
日志式注釋。有了代碼版本控制工具,不必在文件開頭維護(hù)修改時(shí)間、修改人這類日志式的注釋
能用函數(shù)或者變量表示就別用注釋 :
// does the module from the global list <mod> // depend on the subsystem we are part of? if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())可以改為
ArrayList moduleDependees = smodule.getDependSubsystems(); String ourSubSystem = subSysMod.getSubSystem(); if (moduleDependees.contains(ourSubSystem))
位置標(biāo)記。標(biāo)記多了會被我們忽略掉 :///////////////////// Actions //////////////////////////
右括號注釋
try { while () { if () { ... } // if ... } // while ... } // try如果你想標(biāo)記右括號,其實(shí)應(yīng)該做的是縮短函數(shù)
署名 /* add by rick */ 源代碼控制工具會記住你,署名注釋跟不上代碼的演變。
注釋掉的代碼。會導(dǎo)致看到這段代碼其他人不敢刪除
信息過多。別在注釋中添加有趣的歷史話題或者無關(guān)的細(xì)節(jié)
沒解釋清楚的注釋。注釋的作用是解釋未能自行解釋的代碼,如果注釋本身還需要解釋就太遺憾了。
短函數(shù)的函數(shù)頭注釋。為短函數(shù)選個(gè)好名字比函數(shù)頭注釋要好。
非公共API函數(shù)的javadoc/phpdoc注釋。
代碼格式 1.垂直格式 短文件比長文件更易于理解。平均200行,最多不超過500行的單個(gè)文件可以構(gòu)造出色的系統(tǒng)
區(qū)隔: 封包聲明、導(dǎo)入聲明、每個(gè)函數(shù)之間,都用空白行分隔開,空白行下面標(biāo)識著新的獨(dú)立概念
靠近: 緊密相關(guān)的代碼應(yīng)該互相靠近,例如一個(gè)類里的屬性之間別用空白行隔開
變量聲明應(yīng)盡可能靠近其使用位置 :循環(huán)中的控制變量應(yīng)該總是在循環(huán)語句中聲明。
成員變量應(yīng)該放在類的頂部聲明 ,不要四處放置
如果某個(gè)函數(shù)調(diào)用了另外一個(gè),就應(yīng)該把它們放在一起 。我們希望底層細(xì)節(jié)最后展現(xiàn)出來,不用沉溺于細(xì)節(jié),所以調(diào)用者盡可能放在被調(diào)用者之上。
執(zhí)行同一基礎(chǔ)任務(wù)的幾個(gè)函數(shù)應(yīng)該放在一起。
2.水平格式 一行代碼不必死守80字符的上限,偶爾到達(dá)100字符不超過120字符即可。
區(qū)隔與靠近: 空格強(qiáng)調(diào)左右兩邊的分割。賦值運(yùn)算符兩邊加空格,函數(shù)名與左圓括號之間不加空格,乘法運(yùn)算符在與加減法運(yùn)算符組合時(shí)不用加空格(a*b - c)
不必水平對齊。例如聲明一堆成員變量時(shí),各行不用每一個(gè)單詞都對齊。
短小的if、while、函數(shù)里最好也不要違反縮進(jìn)規(guī)則,不要這樣:if (xx == yy) z = 1;
對象和數(shù)據(jù)結(jié)構(gòu) 對象:暴露行為(接口),隱藏?cái)?shù)據(jù)(私有變量) 數(shù)據(jù)結(jié)構(gòu):沒有明顯的行為(接口),暴露數(shù)據(jù)。如DTO(Data Transfer Objects)、Entity
1.對象與數(shù)據(jù)結(jié)構(gòu)的反對稱性(參考書中代碼清單6-5) 使用數(shù)據(jù)結(jié)構(gòu)便于 在不改動(dòng)現(xiàn)在數(shù)據(jù)結(jié)構(gòu)的前提下添加新函數(shù) ;使用對象便于 在不改動(dòng)既有函數(shù)的前提下添加新類 。
使用數(shù)據(jù)結(jié)構(gòu)難以添加新數(shù)據(jù)結(jié)構(gòu) ,因?yàn)楸仨毿薷乃泻瘮?shù);使用對象難以添加新函數(shù) ,因?yàn)楸仨毿薷乃蓄悺?/p>
萬物皆對象只是個(gè)傳說,有時(shí)候我們也會在簡單數(shù)據(jù)結(jié)構(gòu)上做一些過程式的操作。
2.Demeter定律(最少知識原則) 模塊不應(yīng)該了解它所操作對象的內(nèi)部情形
class C的方法f只應(yīng)該調(diào)用以下對象的方法:
C
在方法f里創(chuàng)建的對象
作為參數(shù)傳遞給方法f的對象
C持有的對象
方法不應(yīng)調(diào)用 由任何函數(shù)返回的對象 的方法。下面的代碼違反了demeter定律:final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
一個(gè)簡單例子是,人可以命令一條狗行走(walk),但是不應(yīng)該直接指揮狗的腿行走,應(yīng)該由狗去指揮控制它的腿如何行走。
異常處理 異常處理很重要,但如果異常處理四處分散在代碼中 導(dǎo)致邏輯模糊不清,它就是錯(cuò)的。
1.使用異常而不是返回錯(cuò)誤碼 2.先寫try-catch語句 3.使用unchecked exception(java獨(dú)有) 4.在catch里盡可能的記錄錯(cuò)誤信息,記錄失敗的操作以及失敗的類型。 5.根據(jù)調(diào)用者的需要 定義不同的異常處理類 6.特例模式: 創(chuàng)建一個(gè)類來處理特例。 try { MealExpendses expenses = expenseRepotDAO.getMeals(employee.getID()); m_total += expenses.getTotal();// 如果消耗了餐食,計(jì)入總額 } catch (MealExpensesNotFound e) { m_total += getMealPeDiem();// 如果沒消耗,將員工補(bǔ)貼計(jì)入總額 }
異常打斷了業(yè)務(wù)邏輯??梢栽趃etMeals()里不拋異常,而是在沒消耗餐食的時(shí)候返回一個(gè)特殊的MealExpense對象(PerdiemMealExpense),復(fù)寫getTotal()方法。
MealExpendses expenses = expenseRepotDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); publc class PerDiemMealExpenses implements MealExpenses { public int getTotal() { //return xxx; //返回員工補(bǔ)貼 } }7.別返回null值 8.別傳遞null值 邊界 將第三方代碼干凈利落地整合進(jìn)自己的代碼中
1.避免公共API返回邊界接口,或者將邊界接口作為參數(shù)傳遞給API。將邊界保留在近親類中。 2.不要再生產(chǎn)代碼中試驗(yàn)新東西,而是編寫測試來理解第三方代碼。 3.避免我們的代碼過多地了解第三方代碼中的特定信息。 單元測試 1.TDD(Test-driven development)三定律 First Law: You may not write production code until you have written a failing unit test.
Second Law: You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
Third Law: You may not write more production code than is sufficient to pass the currently failing test.
2.保持測試整潔 3.每個(gè)測試一個(gè)斷言 4.整潔的測試依賴于FIRST規(guī)則 fast: 測試代碼應(yīng)該能夠快速運(yùn)行 ,因?yàn)槲覀冃枰l繁運(yùn)行它。
independent: 測試應(yīng)該相互獨(dú)立 ,某個(gè)測試不應(yīng)該依賴上一個(gè)測試的結(jié)果,測試可以以任何順序進(jìn)行。
repeatable: 測試應(yīng)可以在任何環(huán)境中通過
self-validating: 測試應(yīng)該有bool 值輸出,不應(yīng)通過查看日志來確認(rèn)測試結(jié)果,不應(yīng)手工對比兩個(gè)文本文件確認(rèn)測試結(jié)果。
timely: 及時(shí)編寫測試代碼。單元測試應(yīng)該在生產(chǎn)代碼之前編寫 ,否則生產(chǎn)代碼會變得難以測試。
類 1.類的結(jié)構(gòu)組織: 公共靜態(tài)常量
私有靜態(tài)變量
私有實(shí)體變量
公共函數(shù)
私有工具函數(shù)
2.類應(yīng)該短小 對于函數(shù)我們計(jì)算代碼行數(shù) 衡量大小,對于類我們使用權(quán)責(zé) 來衡量。
類的名稱應(yīng)當(dāng)描述其權(quán)責(zé) 。類的命名是判斷類長度的第一個(gè)手段,如果無法為某個(gè)類命以準(zhǔn)確的名稱,這個(gè)類就太長了。類名包含模糊的詞匯,如Processor、Manager、Super ,這種現(xiàn)象就說明有不恰當(dāng)?shù)?strong>*權(quán)責(zé)聚集情況。
單一權(quán)責(zé)原則: 類或者模塊應(yīng)該有一個(gè)權(quán)責(zé)——只有一條修改的理由(A class should have only one reason to change. )。
系統(tǒng)應(yīng)該由許多短小的類 而不是少量巨大的類 組成。
類應(yīng)該只有少量的實(shí)體變量,如果一個(gè)類中每個(gè)實(shí)體變量都被每個(gè)方法所使用 ,則說明該類具有最大的內(nèi)聚性 。創(chuàng)建最大化的內(nèi)聚類不太現(xiàn)實(shí),但是應(yīng)該以高內(nèi)聚為目標(biāo),內(nèi)聚性越高說明類中的方法和變量互相依賴、互相結(jié)合形成一個(gè)邏輯整體 。
保持內(nèi)聚性就會得到許多短小的類 。如果你想把一個(gè)大函數(shù)的某一小部分拆解成單獨(dú)的函數(shù),拆解出的函數(shù)使用了大函數(shù)中的4個(gè)變量,不必將4個(gè)變量作為參數(shù)傳遞到新函數(shù)里 ,僅需將這4個(gè)變量提升為大函數(shù)所在類的實(shí)體變量 ,但是這么做卻因?yàn)閷?shí)體變量的增多而喪失了類的內(nèi)聚性,更好多做法是讓這4個(gè)變量拆出來,擁有自己的類 。將大函數(shù)拆解成小函數(shù)往往是將類拆分為小類的時(shí)機(jī)。
3.為修改而組織(參考書中代碼清單10-9、10-10) 系統(tǒng) 1.將系統(tǒng)的構(gòu)造與使用分開 將全部構(gòu)造過程搬遷到main 或者被稱之為main的模塊中,涉及系統(tǒng)其余部分時(shí),假設(shè)所有對象都已經(jīng)正確構(gòu)造。
有時(shí)應(yīng)用程序也需要負(fù)責(zé)確定何時(shí)創(chuàng)建對象,我們可以使用抽象工廠模式 讓應(yīng)用自行控制何時(shí)創(chuàng)建對象 ,但構(gòu)造能力在工廠實(shí)現(xiàn)類里 ,與使用部分分開。
依賴注入(DI),控制反轉(zhuǎn)(IoC) 是分離構(gòu)造與使用的強(qiáng)大機(jī)制。
emergent design 四條規(guī)矩幫助你創(chuàng)建優(yōu)良的設(shè)計(jì)
1.運(yùn)行所有測試 2.消除重復(fù) 兩個(gè)方法提取共性到新方法中,新方法分解到另外的類里,從而提升其可見性。
模板方法模式 是消除重復(fù)的通用技巧
// 原始邏輯 public class VacationPolicy() { public void accrueUSDivisionVacation() { //do x; //do US y; //do z; } public void accrueEUDivisionVacation() { //do x; //do EU y; //do z; } } // 模板方法模式重構(gòu)之后 abstract public class VacationPolicy { public void accrueVacation() { x(); y(); z(); } private void x() { //do x; } abstract protected void y() { } private void z() { //do z; } } public class USVacationPolicy extends VacationPolicy { protected void y() { //do US y; } } public class EUVacationPolicy extends VacationPolicy { protected void y() { //do EU y; } }
3.表達(dá)意圖 4.盡可能少的類和方法 5.以上4條規(guī)則優(yōu)先級依次遞減。重要的是測試、消除重復(fù)、表達(dá)意圖。 并發(fā)編程 1.防御并發(fā)代碼問題的原則與技巧 遵循單一職責(zé)原則。分離并發(fā)代碼與非并發(fā)代碼
限制臨界區(qū)數(shù)量、限制對共享數(shù)據(jù)的訪問。
避免使用共享數(shù)據(jù),使用對象的副本。
線程盡可能地獨(dú)立,不與其他線程共享數(shù)據(jù)。