程式碼整潔之道 讀書筆記 - 第3章 函式
短小
函式的第一規則是要短小。第二條規則是還要更短小。
函式20行封頂最佳。
if語句、else語句、while語句等,其中的程式碼塊應該只有一行,而且,塊內呼叫的函式擁有較具說明性的名稱,還能起到文件的作用。
只做一件事
函式應該做一件事。做好這件事。只做這一件事。
每個函式一個抽象層級
自頂向下讀程式碼:向下規則.
switch語句
只出現一次,用於建立多型物件,且隱藏在某個繼承關係中,系統其他部分看不到。
使用描述性的名稱
沃德原則:如果每個例程都讓你感到深合己意,那就是整潔程式碼。
長而具有描述性的名稱,要比短而令人費解的名稱好,要比描述性的長註釋好。
函式引數
理想的引數數量是越少越好,儘量避免三個以上的引數。
引數帶有太多概念性,includeSetupPage()要比includeSetupPageInto(newPage-Content)易於理解。
從測試的角度看,要編寫能確保引數的各種組合執行正常的測試用例。
1、一元函式的普遍形式
1)有輸入引數也有輸出引數
boolean fileExists("MyFile") InputStream fileOpen("MyFile")
2)有輸入引數沒有輸出引數
void passwordAttemptFailedNtimes(intattempts)
3)儘量避免編寫不遵循這些形式的一元函式
void includeSetupPageInto(StringBuffere pageText)
對於轉換,使用輸出引數而非返回值令人迷惑。
如果函式要對輸入引數進行轉換操作,轉換結果就該體現為返回值。實際上,StringBuffer transform(StringBuffer in) 比 void transform(StringBuffer out) 好。
2、標識引數
向函式傳入布林值就是說明本函式不止做一件事。如果標識為true將會這樣做,標識為false則會那樣做。
將render(Boolean isSuite)函式一分為二:renderForSuite() 和 renderForSingleTest()
3、二元函式
1)兩個引數的函式要比一元函式難懂
writeField(name) //一個引數的可讀性更好 writeField(outoutStream, name)
2)有些時候兩個引數正好
Point p = new Point(0, 0)
笛卡兒點天生擁有兩個引數。如果看到new Point(0)會反而感到奇怪。
3)應該儘量利用一些機制將其轉換成一元函式
outputStream.writeField(name) //把writeField方法寫成outputStream的成員之一
4、三元函式
有三個引數的函式要比二元函式難懂得多,建議在寫三元函式前一定要想清楚。
5、引數物件
如果函式看來需要兩個、三個或三個以上引數,就說明其中一些引數應該封裝為類了
Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);
6、引數列表
7、動詞與關鍵字
給函式取個好名字,能較好地解釋函式的意圖,以及引數的順序和意圖。
對於一元函式,函式和引數應當形成一種非常良好的動詞/名詞對形式
write(name) //不管這個“name”是什麼,都要被“write” writeField(name) //更好的名稱,它告訴我們,“name”是一個“field”
無副作用
副作用是一種謊言。函式承諾只做一件事,但還是會做其他被藏起來的事
public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if ("Valid Password".equals(phrase)) { Session.initialize(); return true; } } return false; } }
程式碼的副作用在於對Session.initialize()的呼叫。checkPassword函式,就是用來檢查密碼的。並未暗示它會初始化該次會話。當某個誤信了函式名的呼叫者想要檢查使用者有效性時,就得冒抹除現有會話資料的風險。
這一副作用造出了一次時序性耦合。checkPassword如果在不合適的時候呼叫,會話資料就有可能沉默地丟失。
如果一定要時序性耦合,就應該在函式名稱中說明。重新命名函式為checkPasswordAndInitializeSession,但這還是違反了“只做一件事”的規則。
分隔指令與詢問
函式要麼做什麼事,要麼回答什麼事,但二者不可兼得
public boolean set(String attribute, String value);
該函式設定某個指定屬性,如果成功就返回true,如果不存在那個屬性則返回false。這樣就導致了以下語句
if (set("username", "unclebob")) ...
它是在問username屬性值是否之前已設定為unclebob,或是在問username屬性值是否成功設定為unclebob。
要解決這個問題可以將set函式重新命名為setAndCheckIfExists,但真正的解決方案是把指令與詢問分隔開來,防止混淆的發生
if (attributeExists("username")) { setAttribute("username", "unclebob"); ... }
使用異常替代返回錯誤碼
1、抽離Try/Catch程式碼塊
把Try/catch程式碼塊的主體部分抽離出來,另外形成函式。
2、錯誤處理就是一件事
函式應該只做一件事。錯誤處理就是一件事。因此,處理錯誤的函式不該做其他事。
關鍵字try在某個函式中存在,它就該是這個函式的第一個單詞,而且在catch/finally程式碼塊後面也不該有其他內容。
3、依賴磁鐵
返回錯誤碼通常暗示某處有個類或是列舉定義了所有錯誤碼
public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
這樣的類就是一塊依賴磁鐵(dependency magnet);當Error列舉修改時,所有使用它的類都需要重新編譯和部署。所以程式設計師寧願複用舊的錯誤碼,而不新增新的。
別重複自己
重複可能是軟體中一切邪惡的根源。許多原則與實踐規則都是為控制與消除重複而建立。
考德(Codd)的資料庫正規化都是為消滅資料重複而服務。
面向物件程式設計將程式碼集中到基類,從而避免了冗餘。
面向方面程式設計(Aspect Oriented Programming)、面向元件程式設計(Component Oriented Programming)多少也都是消除重複的一種策略。
結構化程式設計
有些程式設計師遵循Edsger Dijkstra的結構化程式設計規則。Dijkstra認為,每個函式、函式中的每個程式碼塊都應該有一個入口、一個出口。
遵循這些規則,意味著每個函式中只該有一個return語句,迴圈中不能有break或continue語句,而且永永遠遠不能有任何goto語句。
對於小函式,這些規則助益不大,而且偶爾出現return、break或continue語句沒有壞處,甚至還比單入單出原則更具有表達力。