在《Java 8 Lambdas》書中談到:「重構程式碼以採行lambda的這個過程,已經有了個很潮的名稱"point lambdafication"」,這不單只是針對那些群集處理程式碼,進行從外部迭代至內部迭代之類的改造,而是開發者主動從Code as data的角度,嗅出過去一些未察覺或只得忍受的區域性程式餿味,從而進行改造的過程。

錯置的程式碼區塊

在談到point lambdafication之前,可以先回顧一下《Refactoring》書中第一章談的:「絕大多數情況下,方法應該放在它所使用的資料之所屬物件(或說類別)」。書中舉的例子是Customer類別的amountFor方法,其使用了Rental類別的資訊,但沒有使用來自Customer類別的任何資訊,這表示amountFor方法放錯了位置,在將它移至Rental之後,可以去掉不必要的參數並適當命名,從軟體度量角度來看,這是一種改善內聚力(Cohesion)的過程,也就是將相關程式碼組合在同一模組的過程。

除了方法之外,更小粒度的程式碼區塊,有時也會出現座落於客戶端,卻極少使用到任何客戶端資訊的情況。

舉例來說,使用Logger時,雖然呼叫debug等方法時,會自動在層級符合時才進行日誌的動作,不過有時日誌時要輸出的訊息,涉及昂貴運算時,為了避免效能問題,經常會看到這類程式碼區塊:

if(logger.isDebugEnabled()) {

    logger.debug(buildExpensiveMessage());

}

雖說是個很短的程式區塊,但這使用模式會經常出現在有日誌需求的各個角落,程式碼中使用了Logger的isDebugEnabled方法,在條件成立的情況下,繼續使用的方法仍是Logger上的debug,只是傳給debug方法的「程式碼」不同,然而,這段程式碼區塊放錯位置了嗎?

在JDK8中是這麼認為的,因此,有個重載後的debug方法可以使用,例如logger.debug(() -> buildExpensiveMessage()),當然,在過去的JDK版本中本也可以實現這個debug方法,只不過在使用匿名類別會造成語法繁瑣的情況下,使得開發者寧願忍受這一類區域性的程式餿味。除了Logger之外,JDK8的Map新增computeIfPresent、computeIfAbsent等方法,也是類似的重構結果。

舉computeIfPresent為例,因為發覺在客戶端應用Map的場合中,程式碼區塊中使用的,多是來自Map的get、put與remove方法,唯一不同的是計算新值時使用的「程式碼」,這表示此場合中的程式碼區塊,應該放入Map之中,因而在JDK8中,可以使用map.computeIfPresent(key, (key, old) -> computeNewValue(key, old))來取代。

如果程式碼區塊已重構為方法並置入適當類別,且重構後的方法可在多個類別共用,可考慮提升為介面的預設方法,由需要共用行為的類別來實現。

重寫父類方法的匿名類別

當然,過去版本的JDK平臺上,有不少場合使用了匿名類別,舉例來說,ThreadLocal可讓各執行緒存取各自擁有的值,為實作執行緒安全的一個方式,過去,開發者為了確保set方法被呼叫前,就有初值可被get取回,他們必須設法繼承ThreadLocal後,重寫initialValue方法。例如:

ThreadLocal<Resource> resource = new ThreadLocal<Resource>() {

    protected Resource initialValue() {

        return res.lookupResource();

    }

};

若以先前描述的觀點來看,除了return res.lookupResource()之外,程式碼繼承了ThreadLocal建構式、重寫了initialValue方法並建立了ThreadLocal實例,這些都算是ThreadLocal上的資訊。

實際上,為了可以隱藏建構實例的細節,馬上可以聯想到在ThreadLocal上設計工廠方法,只不過在過去,為了將return res.lookupResource()這程式碼當作資料傳入工廠方法時,鬼打牆似地,開發者還是得使用匿名類別,這使得工廠方法的實現完全失去意義。

在JDK8的Lambda語法支援下,ThreadLocal新增了withInitial方法,同樣的需求在JDK8中,可以實作為:

ThreadLocal<Resource> resource =

    ThreadLocal.withInitial(() -> res.lookupResource());

重寫父類方法的匿名類別,實際上暴露了物件建構的細節,也暴露了父類別的細節,因為開發者得知道要重寫哪個方法。開發者只是想要物件能按照提供的特定行為來運作,也就是能傳遞特定行為、程式碼給物件,而這是JDK8中Lambda表示式的目的,因而在JDK8中,無需再忍受這類匿名類別的餿味。

實作函式介面且無值域的類別

有時,開發者想實作的介面只定義一個方法,而實作該介面時不需任何值域,這種情況在過去也常見使用匿名類別實現。在JDK8中,只定義一個方法的介面符合函式介面的語法規範,通常其存在目的,是作為傳遞行為給方法時的目標型態,因此將匿名類別改為Lambda表示式,是接觸過JDK8稍有時間的開發者,基本上都會使用的技倆。

例如《Java 8 Lambdas》中的例子,在實現命令(Command)模式時,若macro的record方法接受Action實例,而Action是只定義一個方法的介面,那麼,就可以使用macro.record(() -> editor.open()),而不是匿名類別。

然而,過去如果有多處出現了相同的匿名類別程式碼,基於程式碼重用,開發者可能會選擇獨立地定義出一個類別,例如以上macro的例子,可能定義出Open類別來實作Action介面,並在多處使用macro.record(new Open(editor))之類的程式碼,如此想修改行為時,就只需要修改Open類別。

這類情況,若在JDK8中於多處撰寫macro.record(() -> editor.open()),雖說語法簡潔了一些,但並不是好的解決方式,畢竟Lambda表示式本身的行為還是有重複問題。

當相同的Lambda表達式在多處重複出現時,就是以方法參考取代Lambda表達式的時機。以上例而言比較單純,因為實際上只呼叫了editor.open(),因而可以直接使用macro.record(editor::open),如果重複的程式碼區塊包括控制流程,可以將之提取獨立為一個方法,之後就能採用macro.record(Macro::markdownToHtml)之類的方式,如果想修改行為時,就只需要修改markdownToHtml方法。

綜合來說,過去如果實作的介面符合函式介面語法規範,在lambda改造的過程中,可以考慮去殼,像是去除匿名類別改為lambda表達式,或是去除具體類別改使用方法參考。當然,lambda表達式建議使用於簡單的行為傳遞,如果想傳遞的程式碼更為複雜冗長,不易看出程式意圖之時,即使只有一個lambda表達式,也可以抽取為一個方法,並使用適當的方法名稱,來突顯整個程式碼的目的。

Code as data

在談論Lambda的一開始,常著眼在那些將外部迭代改為內部迭代的群集處理,抽取出那些高階的行為重複,確實地,迴圈、群集處理有許多常見的行為重複,也是Lambda改造的重點。

不過,除了這些之外,仍有不少更小粒度的程式碼區塊,在過去因為缺少Lambda表示式的情況下,而有著令開發者看不出或必須忍受程式碼的餿味。

實際上,這些程式碼區塊,多半是涉及到行為的傳遞,也就是Code as data的實現,隨著開發者越來越能從Code as data的角度出發,也就越能發掘出更多point lambdafication的候選對象與重構手法。

專欄作者

熱門新聞

Advertisement