第3章:函式

仍舊先看一下書中的例子,直接跳到原文第一段落的修改成品。(直接看此例,是不是會有一些疑問呢?確實是還有斟酌空間的)

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
 if (isTestPage(pageData)) {
   includeSetupAndTeardownPages(pageData, isSuite);
  }
 return pageData.getHtml();
}

簡短

首要原則就是兩個字「簡短」,很簡單,看大綱一定比看原文快。 怎麼做? 記得單一職責原則,然後重構、重構再重構。

架構與流程

概念層次與程式段落

在長篇大論的函式程式碼中,若我們可以合理的把它區分為幾個段落/步驟,那麼,它就可以被拆解。 要怎麼評判一個函式符合單一職責原則?看書中的例子 renderPageWithSetupsAndTeardowns 做了一件事?還是三件事?

  Determining whether the page is a test page.
  If so, including setups and teardowns.
  Rendering the page in HTML.

就宏觀的角度看,可以說只有一件事,就是產出 HTML page 的內容。所以若我們把例中的 method 通通做 inline 處理,就會得回本章一開始那個落落長的函式範例(listing 3-1)

正向來看,一個個用 extractMethod 分離出來的method就是它的子步驟,所以每一個用 {} 包起來的程式段落都是可能可以被拆解出來的侯選人。但回過頭來,怎麼定義步驟與其子步驟就是個權衡的問題。像是上例被切割成三個步驟,但難道不能分為八個嗎?

拾級而下(The Stepdown Rule)

自然的閱讀習慣是由上而下,所以若每一個段落的產出,就是用下一段落的輸入,這會讓人很容易的理解程式想要表述的內容。可以參考一下依此法則撰寫的專案原始碼:

    protected Map<IRole, List<MenuDetail>> processLoadConfig(UdeProperties properties) {
        final Set<String> nodes = extractNodes(properties);
        final Map<String, String> parentOfNodes = evalParents(properties, nodes);
        final Map<IRole, List<MenuDetail>> menus = makeMenus(properties, parentOfNodes);
        return menus;
    }

結構化程式設計:一進一出?

不要有 GOTO、break、continue、一個函式只有一個return點,這是傳統結構化程式設計的嚴格準則。 然而函式足夠短小時,return、break、continue這些跳躍流程並沒有壞處,有時反而更清楚的表達所需的邏輯。GOTO 則是另一個情況,因為它通常適用於大型函式,而大型函式則是應該被避免的。

還是不應該跨多層迴圈的 break/continue,對於這種邏輯需求,多半應該把內層迴圈抽出為函式。

SWITCH

Switch 跟GOTO 一樣,也是不樂見於出現在函式中的控制指令。

首先,它很難保持簡短,因為它就是要處理 N 種情境。再來,它與其它程式段落間的藕合性過高,任何一種情境的異動或增減,我們都會需要重新檢視此函式。最糟的問題是,就是在不同的函式間,重複同樣的SWITCH結構,這意味著上述的問題,已經被複製甚或放大了。

另外談到關於Switch敘述本身,在JAVA5以後,我原則上傾向switch 一定要搭配enum 使用。因為 int 的開放性過高,我們很難確認有多少情境是疏漏而未處理的。

合理的修正方案,是使用多型子類,或是 ENUM 處理此類需求。

當然,有一些例外狀況是可以容忍使用的。書上的說法是: they can be tolerated if they appear only once, are used to create polymorphic objects, and are hidden behind an inheritance relationship so that the rest of the system can’t see them.

同理,還有一連串的(if/else if/else)也是可以被考慮修正的對象。

Method signature

參數

先說說參數,再討論命名。

事不過三,寫程式時想著這句就對了,本書認為函式的參數數量最多三個就好。當有一個函式的參數過多時,它可能要變成一個小型的輔助類別。 varargs 可以把它視為一個參數,但是在使用上,請參考「Item 42: Use varargs judiciously」(Effective_Java_2nd_Edition)的一些注意事項。

一來,過多的參數容易令人混淆。二來,在單元測試中組合所有輸入參數的狀況是很困難的任務。試想若參數X有N種可能值、參數Y有M種可能值,那麼 foo(x,y) 跟 fooX(x) / fooY(y), 那一個比較容易寫出完整的測試案例? 最重要的,在我們理解整個程式時,每多增加一個參數,我們就要去了解它是由何而來,作用為何,它代表的意義是否是我們想要的,是否有人誤用;而有些參數可能只是個過客,在當下層次的邏輯中是不重要的角色。

有些情況很難避免多參數程式,我個人的基本準則是,至少同一型別的參數盡量不重複。 例如 spring-mvc 的 controller 處理函式,我們可能會有 request / response / input DTO / 驗證器回傳的驗證結果等等項目存在輸入參數中。

其它關於參數的重點有:

避免使用 BOOLEAN 參數
避免使用 輸出型 參數

名稱與行為

函式的名字,就是一個動詞+名詞的子句。所以命名上可以呈現其參數所代表的意義。

如:assertExpectedEqualsActual(expected, actual)

不要偷偷做事。一個名字叫 boolean checkPassword(...) 的函式,就只要檢查密碼就好,不要做 Session.initialize() 這種事。如果它的任務如此,這個函式的名字就應叫做 checkPasswordAndInitializeSession 。

一次只做一件事,區分 query 與 command 類型的函式。

 if (attributeExists("username")) {
   setAttribute("username", "unclebob");
   ...
 }

如果不在意attributeExists的結果,整段 extra-mathod 為 replaceExistedAttribute 呢!?

回傳值

原則上,就是「以例外取代回傳錯誤碼」,但我認為第三章關於這個議題討論的不夠周全。 可以參考本書第七章及其它書籍關於例外處理的專門論述,屆時再一併整理心得。

其它心得

一段書上的CODE

private void includeSetupPages() throws Exception {
  if (isSuite){
    includeSuiteSetupPage();
  }
  includeSetupPage();
}
private void includeSuiteSetupPage() throws Exception {
  include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception {
  include("SetUp", "-setup");
}

跟inline處理後的CODE,哪一個使閲讀者較易理解!? 見仁見智。

private void includeSetupPages() throws Exception {
  if (isSuite){
    includePage(SuiteResponder.SUITE_SETUP_NAME, "-setup");
  }
  includePage("SetUp", "-setup");
}

results matching ""

    No results matching ""