高質(zhì)量代碼的特征
回想起來,我覺得我們似乎在誤讀Uncle Bob的Clean Code,至少我們錯誤地將所謂Clean與可讀性代碼簡單地劃上了等號。尤為不幸的是,在Clean Code一書中,從第二章到第五章都圍繞著可讀性代碼做文章,于是加深了這種錯誤的印象。
許多具有代碼潔癖的程序員將代碼可讀性視為神圣不可侵犯的真理,并奉其為高質(zhì)量代碼的最重要特征,封上了“神壇”。殊不知,Uncle Bob在Clean Code的***章就通過別人之口對所謂“Clean Code”進行了正名:所謂整潔代碼并非僅僅是“清晰”這么簡單。
按照Kent Beck的簡單設(shè)計規(guī)則,排在***位的其實不是可讀性,而是“通過所有測試”。其中潛藏的含義是滿足用戶正確的需求,因為測試可以看做是用戶提出的需求。這個需求不僅僅是業(yè)務(wù)上的,還包括質(zhì)量屬性的需求,例如性能、安全等屬性。
消除重復(fù)和提高表達力這兩點,有時候會互相促進,去除了冗余的代碼,會讓代碼變得更加清晰;然而,有時候卻又互相沖突,消除重復(fù)的成本可能會比較高,導(dǎo)致提取了太多細碎微小的實體,反而增加了閱讀障礙。
故而我常常將Uncle Bob提出的“函數(shù)的***規(guī)則是要短小。第二條規(guī)則是還要更短小。”看做是一種矯枉過正的強迫。對于那種喜歡編寫大函數(shù)的程序員而言,確實需要時刻銘記這一原則,但切記不要將其視為***準則。保證函數(shù)短小是有前提的,仔細閱讀Kent Beck的簡單設(shè)計原則,依其重要順序:
- 能通過所有測試;
- 沒有重復(fù)代碼;
- 體現(xiàn)設(shè)計者的意圖;
- 若無必要,勿增實體(方法、函數(shù)、類等)。
如果程序滿足了客戶需求,沒有重復(fù)代碼,函數(shù)的表達已經(jīng)足夠清晰地體現(xiàn)設(shè)計者意圖,為何還要不斷地提取函數(shù),使得函數(shù)變得極為短小呢?真正有意義的原則是“讓函數(shù)只做一件事情”。
正因為此,在Clean Code書中,Uncle Bob展示的對FitNesse中HtmlUtil.java的第二次重構(gòu)并無必要。在經(jīng)過***次重構(gòu)后,代碼如下所示:
- public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
- boolean isTestPage = pageData.hasAttribute("Test");
- if (isTestPage) {
- WikiPage testPage = pageData.getWikiPage();
- StringBuffer newnewPageContent = new StringBuffer();
- includeSetupPages(testPage, newPageContent, isSuite);
- newPageContent.append(pageData.getContent());
- includeTeardownPages(testPage, newPageContent, isSuite);
- pageData.setContent(newPageContent.toString());
- }
- return pageData.getHtml();}
這段代碼的結(jié)構(gòu)與層次已經(jīng)非常清晰,也對實現(xiàn)細節(jié)做了足夠合理的封裝與隱藏。若要說不足之處,或許可以將如下代碼再做一次方法提取,使其滿足SLAP原則(單一抽象層次原則):
- newPageContent.append(pageData.getContent());
- //提取為:
- includeTestContents(testPage, newPageContent)
而Uncle Bob做的第二次重構(gòu),除了將方法變得更加短小,隱藏了太多細節(jié)從而引入更多層次之外,究竟給代碼的清晰帶來了什么呢?
- public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
- if (isTestPage(pageData))
- includeSetupAndTeardownPages(pageData, isSuite);
- return pageData.getHtml();
- }
過猶不及啊!
有時候,為了去除重復(fù),就必須要從相似代碼中尋找到一種模式或者某種抽象,進而對其進行提取。過分的提取反而會讓代碼變得很難閱讀,這是因為提取的手段常常會引入“間接”。正如Martin Fowler所說:“間接性可能帶來幫助,但非必要的間接性總是讓人不舒服”。不必要的間接常常妨礙代碼的直截了當和干凈利落。倘若去除重復(fù)帶來的唯一好處僅僅是避免一個類中少許的私有重復(fù),去除這樣的重復(fù)其實意義真的不大。
我喜歡清晰的代碼,但我認為保持代碼的正確、健壯與高效同樣重要。
因為代碼潔癖的緣故,我曾經(jīng)將大量的非空判斷、非法檢查與異常處理視為干擾清晰代碼的洪水猛獸,但如果不做這些“臟活累活”,代碼就可能變得不健壯。在Java中,若真要避免這些判斷,可以考慮轉(zhuǎn)移職責,通過定義Checked Exception,將異常處理的職責轉(zhuǎn)移給方法的調(diào)用者。然而,職責的盲目轉(zhuǎn)移始終是不負責任的。實現(xiàn)每個方法和每個類的程序員應(yīng)該保證自己的代碼是自治的。
如下代碼:
- @Override
- public void run() {
- if (isFromFile) {
- if (hasQuery) {
- throw new RuntimeException("both --execute and --file specified");
- }
- try {
- query = Files.toString(new File(clientOptions.file), UTF_8);
- hasQuery = true;
- }
- catch (IOException e) {
- throw new RuntimeException(format("Error reading from file %s: %s", clientOptions.file, e.getMessage()));
- }
- }
- }
這樣的代碼確實談不上優(yōu)雅,然而足夠充分的判斷保證了代碼的正確性與健壯性。我只能說,在滿足了這兩點的前提下,可以聰明地利用諸如防御式編程、Optional來規(guī)避多余的嵌套或分支,從而提高代碼的可讀性。
Effective Java總結(jié)了高質(zhì)量代碼的幾個特征:清晰、正確、可用、健壯、靈活和可維護。我認為這一總結(jié)非常中肯。寫代碼真的不要太偏執(zhí),不分任何場景一味地追求代碼的可讀(清晰),一味地重申DRY,我覺得都是不負責任的態(tài)度。
或許是我老了的緣故,我變得不再理想主義;但更多的原因是因為我看到太多追求所謂“整潔代碼”的程序,不愿考慮復(fù)雜繁瑣的異外情況從而導(dǎo)致程序的不健壯;因為去除重復(fù)帶來的不必要間接影響了代碼的簡潔與干凈,甚至影響了代碼運行的性能。
整潔代碼是必須的,但不是衡量代碼質(zhì)量的唯一標準!
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者】