about 1 year ago

Translated from "Implementing Design Patterns with Lambdas", Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft, Java Magazine November/December 2016, page 36. Copyright Oracle Corporation.

用 Lambda 實作設計樣式

善用 lambda 可以減少常用程式樣式的實作複雜度

新的語言特性常會讓既有的程式樣式或慣用方式較少使用,例如,在 Java 5 加入的 for-each 迴圈取代多數明確迭代條件的寫法,因為可以減少錯誤並提供更精確的語意;在 Java 7 加入的菱形運算子,<>,建立實體時減少泛型的明確指定 (慢慢讓 Java 開發者接受型態推論)。在本文中,我們將說明 lambda 如何減少實作幾種設計樣式時的程式碼,為了跟上,您需要對 lambda 有基本的熟悉度。

在樣式中有一個特別的類別稱作設計樣式 (design patterns),如果您願意,針對軟體設計中常見的問題,他們是可重複使用的藍圖,有點像建築工程師在某些情境下,有一些可重複使用的方案去蓋橋樑 (像是懸索橋或拱橋之類的),例如,visitor 設計樣式是常用的方案,將演算法與操作的結構分開;另一個樣式, singleton 是限制一個類別只能建立一個物件的常見方案。

Lambda 表示式為開發者的工具箱提供一個新工具,他們為設計樣式要解決的問題提供其他方案,而且通常是更省工的簡單方式,許多既有物件導向的設計樣式可以取代,或用 lambda 表示式以更精簡的方式改寫。接下來,我們探討以下的設計樣式:

  • Strategy
  • Template method
  • Observer
  • Factory

我們說明 lambda 表示式可以為設計樣式原先想解決的問題提供額外的解決方案

Strategy Pattern

Strategy 設計樣式常用在表達一系列演算法,並讓您可以在執行期間選擇它們,您可以在許多情境中使用這樣式,像是根據不同條件檢查輸入、不同的分析方式或是為輸入進行格式化。

如 Figure 1 所示,strategy pattern 包含三個部分。

Figure 1. The strategy design pattern

三個部分分別是:

  • 一個介面表達某個演算法 (即 Strategy 介面)
  • 一個或多個對介面的具體演算法實作 (即 ConcreteStrategyAConcreteStrategyB 類別)
  • 一個或多個用呼使用這些物件

假設您想驗證文字輸入是否依不同條件正確格式化的 (例如,只有小寫或數字),您從定義一個驗證文字 (以 String 表示) 的介面開始:

public interface ValidationStrategy {
    boolean execute(String s);
}

第二步,您定義一個或多個該介面的實作:

public class IsAllLowerCase implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

public class IsNumeric implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

於是您可以在程式中使用不同的驗證機制:

public class Validator {

    private final ValidationStrategy strategy;

    public Validator(ValidationStrategy v) {
        this.strategy = v;
    }

    public boolean validate(String s) {
        return strategy.execute(s);
    }
}

然後,有這些程式後,第一個例子回傳 false,第二個則是 true

Validator v1 = new Validator(new IsNumeric());
System.out.println(v1.validate("aaaa"));

Validator v2 = new Validator(new IsAllLowerCase());
System.out.println(v2.validate("bbbb"));

您應該會發現 ValidationStrategy 是個 functional interface [譯註:只有一個函式的介面],這意味著,您可以將直接將 lambda 表示式傳入,取代以宣告新類別來實作不同策略的方式,也更加簡潔:

// with lambdas

Validator v3 = new Validator((String s) -> s.matches("\\d+"));
System.out.println(v3.validate("aaaa"));

Validator v4 = new Validator((String s) -> s.matches("[a-z]+"));
System.out.println(v4.validate("bbbb"));

如您所見,lambda 表示式移除了從 strategy 設計樣式中繼承而來千篇一律的程式碼,如果您這樣思考,lambda 表示式封裝了一段程式碼 (或策略),而那正是 strategy 設計樣式當初的目的,因此,我們建議您使用 lambda 表示法解決的問題。

Template Method Pattern

當您需要描述一個演算法的大綱,且有額外的彈性可以改變其中一些部分,template method 設計樣式是常見的方案,換句話說,當您發現您在一個情境,例如:我喜歡使用這個演算法,但我需要修改一些程式碼來做我想做的事,這時 template method 樣式就很有用。

讓我們看這樣式是如何使用的例子,假設您要寫一個簡單的網路銀行的應用程式,使用者輸入一個自訂的 ID,然後應用程式從銀行的資料庫取得客戶的詳細資料,最後做一些事讓客戶開心 [譯註:憑空存入 100 萬?],不同的應用程式為不同的分行對於不同的客戶提供不同的方式讓客戶開心,例如:增加些回饋或是減少紙張作業,您可以寫如下的抽象類別表示一個網路銀行的應用程式:

abstract class OnlineBanking {

    public void processCustomer(int id) {
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy(c);
    }

    abstract void makeCustomerHappy(Customer c);
}

processCustomer 函式為網路銀行演算法提供一個草圖:用 ID 取得客戶資料,然後讓客戶開心,不同的分行可以繼承 OnlineBanking 類別以提供不同的 makeCustomerHappy 函式實作。

您可以用您喜歡的 lambda 解決相同的問題 (為演算法建立一個輪廓然後讓實作嵌入),演算法中您想替換成不同的部分,可以用 lambda 表示式或函式參考的方式表示。

這裡,我們為 processCustomer 函式加入第二個參數,型別為 Consumer<Customer> ,它與先前定義的 makeCustomerHappy 函式相同:

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy.accept(c);
}

不用繼承 OnlineBanking 類別,您便可以用 lambda 表示式直接嵌入不同的行為:

new OnlineBankingLambda().processCustomer(1337, (Customer c) ->
    System.out.println("Hello " + c.getName()
);

這是 lambda 表示式可以幫助您移除設計樣式中千篇一律的繼承的另一個例子。

Observer Pattern

當一個物件 (稱為 subject) 需要自動通知一串的其他物件 (稱為 observers) 某個事件 (例如狀態改變) 發生了,observer 設計樣式是常見的方案,您時常在處理 GUI 應用程式時遇見這個樣式,您為 GUI 元件例如按鈕註冊若干 observers,如果按鈕被點擊,這些 observers 會被通知然後可以執行特定的動作,但 observer 樣式並不局限用於 GUI,例如,observer 設計樣式同樣適用在有多個交易員 (observers) 希望可以對股價 (subject) 的變化作出反應,Figure 2 描繪 observer 樣式的 UML 圖形。

Figure 2. The observer design pattern

我們寫些程式來看 observer 實務上是如何有用,您將為一個應用類似 Twitter 設計與實作一個客製化的通知系統,概念很簡單:幾個報社 (像是《紐約時報》、《衛報》及《世界報》) 訂閱新聞推文,如果有推文含特定關鍵字呼望可以收到通知。

首先,您需要一個 Observer 介面將不同的 observers 群組起來,它只有一個函式,在 subject (Feed) 有新的推文時被呼叫:

interface Observer {
    void notify(String tweet);
}

您現在可以定義不同的 observer (這裡,有三個報社),針對推文中包含的關鍵字有不同的動作:

class NYTimes implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY! " + tweet);
        }
    }
}

class Guardian implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Yet more news in London... " + tweet);
        }
    }
}

class LeMonde implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine, and news! " + tweet);
        }
    }
}

您仍少了關鍵的部分:subject!我們為它定義一個介面:

interface Subject {
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}

subject 可以用 registerObserver 函式註冊 observer,並用 notifyObservers 函式通知 observers 有推文,我們進一步實作 Feed 類別:

class Feed implements Subject {

    private final List<Observer> observers = new ArrayList<>();

    public void registerObserver(Observer o) {
        this.observers.add(o);
    }

    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}

這是相當直覺的實作:保持一個內部的 observer 串列,在有推文時可以通知,您可以建立一個示範應用程式將 subject 與 observer 接起來:

Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");

不意外,衛報會挑選這推文!

您可能會想 lambda 表示是如何用在 observer 設計樣式。注意到實作 Observer 介面的不同類別都提供一個 notify 函式的實作。

他們都只是將一段行為包裝起來等到有推文時質行,lambda 表示式正是為移除這些千篇一律的程式所設計,與其明確建立三個 observer 物件,您可以直接將 lambda 表示式來表達要執行的行為:

f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("money")) {
        System.out.println("Breaking news in NY! " + tweet);
    }
});

f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("queen")) {
        System.out.println("Yet more news from London... " + tweet);
    }
});

您應該總是使用 lambda 表示式嗎?答案是否定的,在我們描述例子中,lambda 表示式運作得很好是因為行為很單純,所以在移除千篇一律的程式碼很有用,但 observer 可能很複雜:它們可能有狀態或定義多個函式,在這情況,您應該使用類別。

Factory Pattern

Factory 設計樣式可以讓您建立物件但不用曝露建立的邏輯,例如,您在為銀行工作,需要一種方式建立不同的金融商品:貸款、債券和股票等。

通常,您會建立一個 Factory 類別,有一個函式負責建立不同的物件:

public class ProductFactory {
    public static Product createProduct(String name) {
        switch(name) {
            case "loan":
                return new Loan();
            case "stock":
                return new Stock();
            case "bond":
                return new Bond();
            default:
                throw new RuntimeException("No such product " + name);
        }
    }
}

這裡 LoanStockBond 都是 Product 的子型別,createProduct 函式可能有額外的邏輯配置建立的商品,這好處是讓您建立這些物件但不用曝露建構子與配置的邏輯,這讓使用者更容易建立商品:

Product p = ProductFactory.createProduct("loan");

在 lambda 表示式中,就像是參考到函式一樣,您可以透過函式參考參考建構子,例如,下面是如何參考到 Loan 建構子的例子:

Supplier<Product> loanSupplier = Loan::new;
Loan loan = loanSupplier.get();

使用這技巧,您可以重寫前面的程式,用一個 Map 應對產品名稱與建構子:

final static Map<String, Supplier<Product>> map = new HashMap<>();

static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}

您現在可以用這 Map 去建立不同產品的實體,就如同您之前使用 factory 設計樣式一樣:

public static Product createProduct(String name) {
    Supplier<Product> p = map.get(name);
    if (p != null)
        return p.get();
    throw new IllegalArgumentException("No such product " + name);
}

這是整潔的方式使用 Java 8 的特性完成和 factory 設計樣式相同的意圖,但這技巧不太好擴展,如果 createProduct 函式需要多種不同的參數傳入產品的建構子,除了簡單的 Supplier,您會需要提供不同的 functional 介面。

這些例子清楚地說明 lambda 可以用在許多平常您不能想過要套用的情境,不過,習慣用 lambda,會讓您的程式碼更短、更清楚且更容易寫。

learn more
The original “Gang of Four” book on design patterns

譯者的告白
就如同作者說的,lambda 表示式不適用在所有的情境,我個人也是不太常用 lambda 表示式來實作設計樣式,畢竟完整的程式不只要考慮功能程式碼,也要同時考慮到測試程式碼,lambda 表示式並不好測試,或是精確地說不適合做單元層級的測試,以這次的幾個例子,要測試都必須與其他物件一起測試,這在當測試有錯誤時不好找問題,不過 lambda 表示式仍是一個很重要的特性!

 
about 1 year ago

看完 2017 年第一本書

終於把《加入遊戲因子,解決各種問題》給看完了,這本書買回家應該也有三、四年了,但處於看到一半的狀態也有三、四年了,最近把它放在背包裡,在捷運通勤的過程中看,看著看著也把它看完了,自己過去參與開發的就是把遊戲與 IM 結合的 App,但看完這本書後的想法是,過去的 App 真的算是 Gamification 嗎?還是只是單純把遊戲與 IM 放在一起而已呢?若是自己在看完這本書後會怎麼設計一個 Gamification 的 IM 呢?這似乎是有個有趣的題目。至於書摘...改天再補吧!

 
over 1 year ago

砍掉重練

在忙了幾個禮拜後,精確來說,加上用反向工程了解目前後台 API 的話,應該算是被既有的後台 API 折磨 (很痛苦地看) 了近二個月吧!終於在全公司策略會議結束後,有機會跟同事討論新的後台 API,不過老實說,對一個已經部署到客戶機房的應用 (含後台、前台與 iOS/Android App),我一直對於是否要砍掉重練這件事採保留態度,畢竟砍掉重練耗損的時間太長了,這讓我想起 Netscape 的故事:砍掉重練最後因為時程的關係輸掉瀏覽器大戰。

在重構到新版本這段時間,客戶有任何問題也是需要提供維護的,若是以既有的架構下去修改,可以用分支的方式將修正一併納入新版本中,若是砍掉重練就沒有這好處,但也可以說沒有維持多個分支的困擾,既然都這樣說了,又有什麼好保留呢?主要是語言的關係,我在猶豫是以既有的 (但不喜歡的) JavaScript 重構舊系統,還是改以自己擅長其他語言該發新版本,語言一旦換了,就勢必變成砍掉重練了,這又讓我想起:犧牲的架構,為了砍掉重練的架構。不過,既然上層已經做出決定了,那就這樣吧!

溺死邊緣

自己在 Agile 的團隊待習慣了,後來到新團隊後一直覺得不自在,不自在的原因有很多,像是團隊對於流程的問題,團隊文化的塑造以及軟體品質的改善,感覺像是視而不見,但就在常常這麼想的時候,突然看到柯先生在 Facebook 的留言:

用在軟體開發好像也成立.
某些環境(例如公事業), 或者有些團隊, 活下去已經是很難的事情, 你跟他講什麼開發方法是不太有用的. 因為資源不公,寒門再難出貴子.
最後一句話也很值得深思.
“真正的精英是努力與底層對話,而假精英才反復證明自己是仙人。”
有些朋友很努力幫助這些環境的人, 他們不斷想一些方法, 讓他們過好一點. 即使被罵是假 agile也無所謂.
但也有些"菁英", 一直在證明她有多神. 但是我們還是很崇拜他們.

所以還是多做事少抱怨好了。不過,話說回來,若一直以這樣的藉口拒絕改變,不也一直無法改變勉強維持活下去的現況嗎?

拖稿拖了很久呢 XD

 
over 1 year ago

Trello 書單

在新年的第一天就跑去天瓏書局晃晃,不過翻了一個多小時的書後,一本書也沒買,不是書貴,和學生時期不一樣,書在工作之後其實不貴,只是沒時間看,房間的書櫃堆著一堆沒看完,甚至沒看過的書,沒想到在 Facebook 上看到 Joey 分享他用 Trello 管理書單,十分有意思,想想自己 2016 下半年其實真的沒有好好地在看書,該是用 Trello 來提醒自己花時間在看書上了。話說當初買 iPad Air 也是說要拿來看書,電子書超多,但只用 iPad Air 看了兩本,眼睛就有點受不了了,加上現在早上通勤時基本上是睡死的狀態,只差不會因為睡著坐過頭而已 XD

降低文章頻率

想想自己下半年看書的時間減少,一個原因是懶,這不可否認,有時候一懶就看好幾個小時的動畫,另一個原因是寫文章需要蠻多時間準備的,在不懶的時候,時間都用在翻譯 Java Magazine 或是寫閒談軟體架構上,特別是閒談軟體架構,平日還要看一些論文或是網路上的技術文章,又或是寫一些範例程式,確保程式是 ok 的,像是尚未完成的 MVC 系列,看論文用了三天中午吃飯後的休息時間,範例程式也用了三個下班後的晚上,所以為了讓自己有更多的時間看書,文章的發佈比例會稍微降低一點點。

 
over 1 year ago

Translated from "Interview with Kent Beck", Andrew Binstock, Java Magazine November/December 2016, page 36. Copyright Oracle Corporation.

訪談 Kent Beck

與 JUnit 之父及 TDD 創造者討論編程與測試以及他如何看測試的演進

Binstock: 我知道您現在在 Facebook 工作,您在那裡的主要工作是什麼?
Beck: 我主要著重在工程教育上,我正式的職稱是技術教練,這意味著我大多數時間是與工程師結對編程 (pair programming) 與對談。

Binstock: 他們是特定有經驗的工程師或剛進入這領域的工程師?
Beck: 所有類型都有。我發現如果我指導特定層級的一群工程師,我會聚焦在他們遇到的瓶頸模式,坦白說,我有點厭倦講相同的故事與解決相同的議題,所以,我為這些議題編寫課程,我們有一個非常好的組織透過課程加速工程師的學習,我們有給大學畢業生的課程,也有課程給正要轉型成技術領導的人,以及課程給從外部招募進來的技術領導人,因為 Facebook 的自有文化,所以如果您用過去習慣的,以命令與服從的方式引導,在這裡是行不通的。

Binstock: 當您在像 Facebook 這樣的地方工作,您可能比大多數工程師能看到不同層級的規模,所以會有什麼不同嗎?如果我問您如何審閱需在規模上多加考慮的程式,您會有什麼不同的說法?
Beck: 這是一個好問題,因為它很難總結,所以我可以給您一些實際的例子。日誌 (logging) 很重要,在某些情況下,效能 (performance) 也很重要,一點點效能的退化可能會讓整個網站掛掉,由於我們試著以有效率地方式使用資本、CPU、頻寬以及任何資源營運,所以有時後餘量非常小,因此,對某些團隊來說,效能大多是可犧牲的,而且只有小幅的增加,我認為那是不太尋常的。日誌完全是為了在某些可怕的事發生後能夠除錯,以傳統的極限編程風格 (extreme programming style) 來說,您不會需要它 (YAGNI),因此不會寫它,當然,當您需要它時,自然就會寫,即便事後不再需要,您仍舊會需要它。

Binstock: 我了解。
Beck: 您需要一個能在服務死後驗屍的選項,這選項是這麼有價值,我過去從沒注意到過。

Binstock: 您們如何將程式簽入到主線?我能想像到有大量的測試在簽入前會套用到那些程式,測試的規模比一般的商業網站要大,真是如此嗎?
Beck: 這不完全是真的,有一個我從經濟學教授學來的原則叫可逆性,假設您有一個複雜的系統對刺激的反應無法預測,Henry Ford 建造那些前所未有的大工廠,複雜的系統,一點點小的改變可能會有巨大的影響,所以他對這的應對是減少工廠可能的狀態,例如讓所有的車都是黑色的,然而不是所有的車都是黑色,因為 Henry Ford 很吝嗇於控制,所以噴漆站很簡單要嘛噴漆要嘛不噴。

這讓整個事情容易管理。但是,Facebook 不能減少 Facebook 的狀態,我們希望能持續增加更多的狀態,這是我們如何連結這個世界,因此,與其減少狀態的數量,我們讓決定是可逆的,在 Henry Ford 的工廠,當您切割某一片鋼材,您無法反悔,當然,我們在 Facebook 也是這麼做,如果您讓一個決定是可逆的,你不需要像您剛說地如此嚴格去測試它,你只需要注意何時去復原或關閉它如果它發生問題。

Binstock: 這是相當有趣的替代方案。
Beck: 不過,也有許多反例。例如,處理金錢的程式就特別嚴格地處理,Linux kernel 也是特別嚴格地處理,因為它會被部署在數十萬的機器上,但改變網站,您有相當多種不同的方式取得回饋,您可以用手動測試取得回饋,您可以在內部使用它來取得回饋,您可以釋出到小比例的伺服器上然後觀察數據,如果有些事開始變混亂,您只需關掉它。

Binstock: 所以不可逆的決定會相當嚴格地執行且極度的測試,而其他的會比較簡單因為可逆性。
Beck: 是的。

JUnit 的開發

Binstock: 我們來談一下 JUnit 的起源,您製作的相當多影片將它記錄下來,所以不只再走過一遍,讓我問一些問題,這些工作當初您與 Erich Gamma 是如何分工的?
Beck: 我們結對編程所有事。

Binstock: 所以您們倆全程參與整個專案?
Beck: 除非坐在一起,不然我們不碰觸程式,這持續好幾年。

Binstock: 那時候您們是用 TDD 的方式開發?
Beck: 是的,很嚴格,我們不曾在沒有一個壞掉的測試案例的情況下加新需求。

Binstock: 很好,所以你們如何在 JUnit 可以執行測試前做測試?
Beck: 自食其力,它初期看起來很醜,您可能需要從命令列開始,然後很快地,您得到足夠的功能讓您很方便地執行測試,然後,有時候您弄壞東西但卻得到誤診的結果,於是您會發現所有的測試都通過,但因為剛剛做的修改,我們沒有執行任何測試,於是您又必須回到一開始。人們應該試試這個練習,這是一個非常有用的練習,自食其力開發一個測試框架測試自己。

Binstock: 您在這之前所擁有的只有 SUnit (JUnit 的前身,Beck 為 Smalltalk 寫的),它好像沒有在寫 JUnit 時提供不只是慨念層級的幫助。
Beck: 不,我們完全從頭開始。

Binstock: 您在 JUnit 5 的角色是什麼?就我所知,您似乎沒有明顯地參與在這次的釋出中。
Beck: 是的,我想幾乎任何事都沒參與是最貼切的說法,事實上,我認為,在某一點上,他們討論如何在一個釋出中做出兩種不同的改變,而我說,讓它們變成兩個不同的版本,所以就一個家長的建議,就是這樣了。

測試優先

Binstock: 您仍然按照嚴謹測試優先的原則工作嗎?
Beck: 不,有時候,是。

Binstock: 是的,談一下您演進到那樣的想法,當我看您的書 Extreme Programming Explained 時,似乎沒有太多餘地,您已經改變想法了嗎?
Beck: 當然,當時我不知道有一個變數存在,它在抉擇自動化測試何時有價值時很重要,就是程式碼的半衰期,如果您在探索模式,您只是想知道一個程式可能怎麼運作,而且大多數的實驗都會失敗或是在幾小時或幾天內被刪除,那 TDD 的好處就不會生效,它還會讓實驗慢下來,增加從『我懷疑』到『我知道』之間的延遲,您會希望這延遲時間越短越好,如果測試能幫助您縮短致時間,那很好,但通常它們讓延遲時間更長,如果延遲時間很重要而且程式碼的半衰期很短,那您不該寫測試。

Binstock: 確實,探索時,如果出錯,我可能會返回,然後寫些測試讓程式朝我預期的方式進行。
Beck: 我學習到很多種回饋的形式,測試只是其中一種回饋的形式,它們確實有很多好處,但根據您所在的處境,它可能會是非常大的開銷,於是您需要抉擇,這是否是折衷提示的情境中的一個方向或其他?人們希望有一個準則,一個絕對的準則,但就我擔心的,那只是草率的思考。

Binstock: 是的,我想超過二十年程式經驗的好處是對一個整體的規則存在巨大的不信任,這規則堅定地適用。
Beck: 是的,這唯一的規則是思考,IBM 在這做得很好。

Binstock: 我回想您曾提過類似的事,例如 getter 及 setters,真的不用先替它們寫測試。
Beck: 我認為那比我說的要特殊一點,這始終是我的策略,沒有任何東西是必要測試的,很多人寫了大量的程式但沒有任何測試,然而他們賺了很多錢,也為整個世界提供服務,所以,很明顯地,沒有任何東西是必要測試的。有許多種形式的回饋,對於測試,抉擇的一個因素是:什麼可能造成錯誤?所以如果您有一個 getter,它只是一個 getter,它不會改變,如果您可能搞砸它,那我們需要不同的對話,測試不會修好這問題。

Binstock: 當初您歸納出 TDD 的規則時,其中一個基石是每次迭代,應該要在功能上有最小可能的增量,這觀點從哪來?最小功能性的增量重要性為何?
Beck: 如果您有一個巨大的,漂亮的意大利蒜味鹹臘腸,然後您想知道需要花多少時間可以吃完它,一個有效的策略是切一小塊來吃,然後做點算數,所以 1 mm 需要 10 秒,然後 300 mm 將需要我花 3,000 秒,可能更多也可能更少,可能有正向的回饋循環,也可能是負向的,或其他事情導致時間改變,但最起碼,您有已經有一些經驗。

就我的觀點,一個熟練的程式工程師與大師的分別是大師絕對不會試著一次吃完整個臘腸,首先,他們總是能完成大的事情,他們會將它切片,這是第一個技巧,知道哪裡您可以切割。

第二技巧是能以有創意的順序完成那些小的切片,因為您可能認為你必須從左到右,但是您並不需要。有些人可能說:『嗯,您必須先寫輸入的程式碼,才能寫書出的程式碼』,我則是說:『我不認同,我可以建立在記憶體中的一個資料結構,然後根據那寫輸出的程式碼』,所以我可以先寫輸入再寫輸出,或是先寫輸出再寫輸入。

如果我有 n 個片段,我有 n 種因式排列組合,這其中有些沒有任何意義,但大多數有,所以一個大師級程式工程師善用這兩個技巧,將片段切得更薄,考慮更多種排列組合作為實作的順序,這兩個技巧沒有任何形式的極限,您總是可以切割更薄的片段,您可以總是思考實作事情的順序以滿足某不同的目的。

如果您告訴我在禮拜五有個 demo,相同的專案,我實作事情的順序會跟如果您告訴我在禮拜五跑一個負載測試,我會將它以不同的方式切割,我也會以完全不同的順序實作,根據我下一個目標是什麼。

Binstock: 若其他因素無法建議更小的切割,最小可能增量可以是個概括的準則去切割?
Beck: 我不相信有最小的切片,我相信總是有辦法將切片變得更小,就像我以為我已經切得夠小了,我總是能找到某個地方或某種方式將它切成一半的大小,於是我會敲我自己:『為什麼我之前沒想到呢?這不是一個測試案例,這是三個測試案例』,然後進行得更順暢。

Binstock: 好吧,如果謹守單一責任原則 (single responsibility principle),在我來看,您可以相當地動態,有非常非常小的運算,然後將上千個 BB 彈大小的函式組合起來,然後說明如何將它們放在一起。
Beck: 如果您要開啟 40 或 50 個類別,那會很糟糕,我會說在某些點上,那違反內聚力,這不是對的方式。

Binstock: 我想我們的方向是一致的,那就是切割會有個極限,更小的切片不會增加價值,反而會侵蝕其他好處。
Beck: 就這樣,今天我會睡一覺然後明天早上繼續,『為什麼我沒想到那個?如果我橫向切割而不是上下,它明顯會做得更好』,類似這樣,在週五的下午走出門外然後回家,對我來說,是一個觸發想法的方法。

開發流程

Binstock: 幾年前,我聽說您建議開發者,開發時應該準備一張紙跟一支筆在旁邊,以記錄開發過程中所做的決定。您暗示我們會被我們所做的紀錄的數量給嚇到,而那真的發生在我身上,當我看越多所收集的項目列表,我越是了解到當中斷發生,要重建中斷前的整個世界,需要仰賴能記住眾多的微小決策,間斷越長,越難以將那些微小決策找回。我有點好奇,您記錄微決策的想法是否有某種方式的演進,讓那些列表更有用,而不只是一個開發意識的練習?
Beck: 沒有,完全沒有,有一件事,我寫下這些決策,是因為我有記憶上的問題,我不太能在腦中記住複雜的程式,甚至是小程式,我可以做好一個結對編程的夥伴,是因為我可以依賴夥伴的記憶,坐下來然後試著寫複雜的大型程式已經不是我可以做的事了。

然而,我依然能寫程式,在 UNIX 命令列上,因為我能看到整個全貌,只要是一行長度的程式,我能夠建立它,像是一次一個命令,我能完成編程的任務,但它不是可以維護的程式,它們都是一行的程式,我做很多的資料探勘,所以如果您說,建立一個可以做 X 的服務,那不是不同年齡、以不同的方式或不同的速度等等的問題,那已經不是我可以獨立完成的事,那真的是讓人沮喪的地獄。

Binstock: 當我在想您所說的,我想起我自己在記錄微決策所做的努力,我有一點賞識新的事物像是 Knuth’s Literate Programming,實際上,您可以在註解中捕捉您正在做什麼,您嚐試做什麼,以及您所做的決策。我在聽過您討論這特別的紀律後,我真的用那種方式工作好一陣子,某種程度上,它真的有幫助,另一方面,它建立許多雜亂的東西,最終我還是要回去移除那些註解,所以我提出來的唯一原因是想知道您是否有更好的方式。
Beck: 我所知的 literate programs 是它不好維護,因為註解、圖解與程式之間的耦合太強,過去我不曾這麼說,但它就是如此,如果我對程式做了點修改,我不只要改程式和測試,還要改某四段描述或那兩個圖解,然後我需要重新收集資料並再次將它畫出來,因此它不能很有效率地維護。如果您有非常穩定的程式,然後您想解釋它,然而它不會有用,而我說的依然是有意義的。[譯註:這一段有兩個代名詞,that 與 it,讓我很猶豫是否翻譯對了?]

Binstock: 我和 Ward Cunningham 有過對談,他提到多年前與您結對編程時,他常常對於您們怎麼達成決策感到驚訝,以及透過問:『什麼事是我們可以做的,能最容易完成特定目標』的方式讓工具演進,如果您總是用可能最簡單的事情去做,在某個時間點,您不需要回去重構程式,讓您能對程式感到自豪,而不是一堆對問題的暫時解,您如何平衡這兩件事?
Beck: 當然,我不是被付錢來自豪的,像是 JUnit,要嘛不寫,要嘛我們寫讓我們自豪的程式碼,我們可以做這樣的抉擇,是因為我們沒有期限也沒有付錢的客戶。

但是經常,即使我不對程式感到自豪,但我的雇主對成果很滿意,是的,他們稱之為能用,然後我能因此得到報酬,所以有其他理由需要去清理那些程式碼。

答案是肯定的,因為您不知道本質,所以您做出局部最佳化的決策,然後您了解本質,學習,接著您會了解到設計應該變成這樣、那樣,然後這樣,於是,您必須做出決定,在何時、用什麼方式或要不要改進您的程式碼,有時候您會做,有時候不會。

但是我不知道替代方案是什麼?人們會說:『如何,我們來進行重構吧?』好啊,當然,所以有替代方案嗎?[譯註:不做重構的替代方案]

我記得我曾在丹麥辦過一個研討會,在一整天激昂的演講中討論迭代的好處,那天快結尾時,一整天坐在前排的一個傢伙,一直看著我,表情越來越不安,他在結束前終於舉起手問:『一次把它做好不是更容易嗎?』我想擁抱他,我說:『以我所擁有的同理心,是的,它可能更容易,我沒有其他說法了』

Binstock: 有趣的問題!
Beck: 我曾經在飛機上坐在 Niklaus Wirth [譯註:1984 獲頒圖靈獎,其著作 Algorithms + Data Structures = Programs 廣為人知] 隔壁,我告訴空服員,我說我們是同事,他會願意我移過去,所以我像是個跟蹤者,我是他的紛絲,我不介意 [譯註:被當跟蹤狂?],如果您有機會坐在 Niklaus Wirth 旁邊,您該坐過去。於是我們開始聊天,我跟聊到 TDD 與漸進式設計,而他的回覆是:『我認為那非常地好,如果您不知道如何設計軟體』

Binstock: 那聽請來很像是 Wirth 會說的話。
Beck: 您只能說:『好吧!是的,我不知道如何設計,恭喜,您知道,但我不知道,所以我能怎麼做?我不能假裝我是您』

今日的測試

Binstock: 讓我聊聊微服務吧!它對我來說,對微服務以測試優先的方式開發會變得相當複雜,某些微服務,為了能運作,需要一大包其他的服務存在,您也是如此認為嗎?
Beck: 這其實跟在一個大的類別與好多個小類別之間做抉擇是一樣的。

Binstock: 沒錯,除了,我猜,您需要用超多的假物件去建立一個系統去測試待測的服務。
Beck: 我不這麼認為,如果它是指令式的風格,您確實需要大量的假物件,在函數式風格中,外部關聯是由呼叫的串連 (call chain) 來集中提供,於是我不認為那是必要,我認為您可以從單元測試得到很高的測試涵蓋率。

Binstock: 現今,UI 與過去相比是多麼重要,在過去與現在,您是如何對 UI 做單元測試?您有用過像 FitNesse 或其它框架,或您只是用眼睛檢查測試的結果?
Beck: 我沒有滿意的答案,讓我這樣說,我試過許多東西,我建立過整合的測試框架,我使用過別人的工具,我試過不同的方式去收集 UI 外觀,以可以測試的方式,但沒有一樣可行。

Binstock: 如今,您仍然處在相同的情境,是嗎?
Beck: 是的,我沒看到什麼從根本上改變,所有都跟誤判 (false positives/negatives) 有關,測試告訴您所有的東西都正常以及所有的東西都壞掉的比例是如何?這會對測試的信任造成傷害,測試框架有多常告訴您有東西壞了,但其實所有的事都是正常的?很常,一個像素非常細微的改變,然後測試全壞了,然後您必須一個一個走遍測試,『喔,不,這是好的』,到您拆掉那些測試之前,您不會太常去做那件事。

Binstock: 代價是損失時間與失去信心。
Beck: 是的。

開發環境

Binstock: 現今您喜好的開發環境是什麼?不論是在家或在工作
Beck: 任何像是開發的事情,我都在 UNIX 的命令列或 Excel 中完成。

Binstock: 在 Excel 中?
Beck: 是的,因為我可以看到所有的東西。

Binstock: 怎麼說?
Beck: 像是轉換,我在 UNIX 命令列中做資料轉換,像是數字轉數字,然後我用 Excel 將它們畫成圖片。

Binstock: 當您在進行非資料探勘相關的編程時,如您之前提及的,您仍然使用 Smalltalk 做探索之類的工作。
Beck: 是的,對我來說,Smalltalk 的好處是我記住 API 足夠長的時間,我仍能掌握所有的細節。

Binstock: 您一貫使用多螢幕嗎?
Beck: 是的,越多像素越好,Terry Pratchett 有個很好的說法,他說『人們問我為什麼有六台螢幕接到我的 Mac,然後我告訴他們,因為我無法連接八台』,Oculus 或其他虛擬實境的技術開始要吹起漣漪,但沒人知道是哪種形式。

Binstock: 我們勢必將經過好幾次的迭代,在虛擬實境實際找到協助編程的角色之前。
Beck: 是的,我深深相信可以擺脫文本式的原始碼,然後能直接操作抽象語法樹,我曾和我朋友 Thiago Hirai 一起做一個稱作 Prune 的程式編輯器的實驗,它看起來就像是一個文字編輯器,顯示一個文字編輯器該有的內容,但是您只能對抽象語法樹進行操作,而它真的比較有效率,更不容易出錯,它不需要太多認知上的努力,讓我深信那是未來的浪潮,我不知道它是否能在 5 年內或 25 年內成真,但我們將很快在未來某時間點能操作語法樹。[譯註:想法真的很特別,我則是無法想像該怎麼操作抽象語法樹]

Binstock: 是的,所有的事情都在改變與往前進,但我們真的沒有從用墨水和紙張寫程式的程度邁進多少。
Beck: 不,我們仍在打洞卡上寫程式,它只是在某一層上再畫上一層,但它還是相同的東西。

Binstock: 程式員活動的地方和初始相比沒有進步太多,除了有不錯的 IDE 和其他這類的東西,實際的行為仍是相同的,最後一件事,我知道您是一位音樂家,您在寫程式時聽音樂嗎?
Beck: 當然。

Binstock: 什麼樣的音樂,您會喜歡在寫程式的時候聽?
Beck: 我用它來調節我的能量水平,所以,如果有點激動,我會聽一些舒緩的音樂,我的 go-to 音樂是 Thomas Tallis 的 The Lamentations of Jeremiah,是一個非常低調的聲樂四重奏中世紀音樂,如果我有點低迷,我需要把自己帶起來,於是我聽 go-go 音樂 [譯註:我對音樂不熟,不確定 go-go 是一個名詞還是指精神充沛的],那是原產於華盛頓特區的一種鄉土爵士樂。

Binstock: 好吧,我從沒聽過。
Beck: 那是專屬我的音樂。

Binstock: 太棒了,謝謝您!

譯者的告白
這一篇翻得很辛苦,沒想到像是閒話家常的對談,比技術文章還更難翻譯,這應該也是因為生活詞彙實在太少了,所以一些看起來很短的句子,完全不知道該怎麼翻譯的很生活化,更不用說對話中的代名詞,要想半天了。

 
over 1 year ago

世紀帝國與 Agile 軟體開發

很可惜沒搶到第一波的 PS4 Pro,所以到今天仍沒買到 PS4 Pro 犒賞自己,工作之餘,最大的休閒很靜態,看動畫,因為和玩遊戲相比,時間比較好控制,但有時候即便下班後很疲倦,仍會玩一下遊戲。最近稍微比較常玩的是世紀帝國 II,雖然已經是很有歷史 (我大學時) 的遊戲,但 Steam 上能用蠻便宜的價格買到 HD 版,只是以前是學生,玩遊戲時不會有什麼特別的想法,現在玩著玩著卻發現,其實這遊戲的過程跟 Agile 軟體開發的過程有些地方很像。

村民則是 cross-functional 的開發團隊成員,砍柴、挖礦、採集食物、種田樣樣都能做,出外打仗可能很弱,但非不得已還是能上戰場。即便如此,食物、木材、石礦、金礦以及時間,都是非常重要的資源,而要開發的項目眾多,有的能讓採集 (開發) 速度加快,有的是讓攻擊力 (產品優勢) 增加,還要注意進化 (石器 -> 城堡 -> 帝王) 要能領先對手,最重要的是,能在最後勝過所有對手。

每一個開發項目都需要消耗資源,因此會相互排擠,就跟軟體開發一樣,若是個好 PO,能在衡量重要且緊急、重要但不緊急、不重要但緊急、不重要也不緊急等因素後,根據不同的產品屬性,有些項目甚至是沒有用的,決定開發項目的優先順序就非常重要,在加快開發速度、增加產品優勢、讓團隊進化中抓到最佳的平衡點,不讓團隊瞎忙,用最善用資源的方式,以整體優勢去戰勝對手。

大概就是因為時間都用在寫這種文章,所以才會是一個人跨年吧,祝大家新年快樂~

 
over 1 year ago

中文不像中文,英文不像英文

趁著連假,終於把《深入探討 JUnit 5 的擴充模型》給翻譯完了,但不管是翻譯的過程中,或是整個翻譯完後在校對時,總覺得這中文讀起來不像中文,或是說沒有中文文章的感覺,之前有看過一個說法:翻譯其實是一種再創作,也就是在文意不變的情況下,句子與文字的調整便是翻譯者的創作,但文意與創作的拿捏就挺難的。

除了翻譯,自己也在寫文章,寫的過程中,有時候會覺得受英文作文訓練的影響,句子好像有點像英文又不像,但實際上什麼樣子是英文句型什麼又是中文句型,自已也說不準,只能說接下來新的一年會寫更多東西,對自己寫文章有些期許:能簡單明確,若是科普類的文章,能夠自然帶入輕鬆與趣味,最後,文字能嚴謹洗鍊,自勉之。

文人相輕

前陣子在 Facebook 上看到一位應該也是業界人士引用母校前助理教授在 Slideshare 的投影片,留下一堆有貶意的發言,但看完後我有點傻眼,為什麼會把雲端儲存單純與 NFS 相較?把透過 Gateway 將 sensor 回傳回 server 就當成是 IoT 的全部?怎麼看都覺得這位業界人士會不會看太淺了?

雖然目前的科技都是建築在過去的科技上,很多慨念也都是延續著舊概念,但當要建構一個像 AWS S3 這樣的儲存平台給眾多的服務使用,要考量的事就不是原本 NFS 能夠完成的;即使有 Gateway 也不能完全處理掉 sensor 無法上網的問題,在荒山野嶺或是移動的車輛上,Gateway 自身都不見得能有完整的網路功能,更何況 IoT 看的是應用,在設計平台時,更要考慮如何滿足最多數的可能性,這都需要相當多的工夫在裡面,若能了解這些,實在沒必要留下這麼貶意的發言。

文化養成很難,崩壞卻很快

因為社群的關係,每個月都會有與前同事碰面的機會,因此常會聽到一些有趣的事,事後想想,自己現在一直想在目前的團隊建立敏捷的文化,因此很能體會建立一個文化不是件容易的事,要花上不短的時間,而且如果文化沒有深植入團隊的每個人心中,一旦關鍵的人離開,文化的崩壞卻異常地快啊~

 
over 1 year ago

Translated from "A Deep Dive into JUnit 5’s Extension Model", Nicolai Parlog, Java Magazine November/December 2016, page 25. Copyright Oracle Corporation.

深入探討 JUnit 5 的擴充模型

JUnit 如何執行測試以及如何和函式庫與框架互動的內幕

JUnit 的下一個釋出是第五版,是 Java 最廣泛使用的測試函式庫,一個重要的釋出,這重要的釋出主要提供一個新的架構,將 JUnit 工具與平台分開,以及一個新的擴充模型排除過去架構的關鍵限制。

在本文中,我探討第三方函式庫與框架用來與 JUnit 互動或擴充它的擴充模型,這主題主要適合工具與函式庫的開發者,同樣適合想精通 JUnit 如何運作的開發者,要跟上,您將需要對 JUnit 4 有足夠的了解。

我應該加上,會想花時間了解擴充模型的應用程式開發者是那種想減少樣板程式與提升他們測試的可讀性與維護性的開發者。

測試的生命週期

擴充與測試的生命週期息息相關,以下面的測試為例,所以我們先看測試的生命週期:

// @Disabled <1.>

class LifecycleTest {

    LifecycleTest() { /* <2.> */ }

    @BeforeAll
    static void setUpOnce() { /* <4.> */ }

    @BeforeEach
    static void setUp() { /* <5.> */ }

    @Test
    // @Disabled <3.>

    void testMethod(String parameter /* <6.> */)

    { /* <7. then 8.> */ }
    @AfterEach
    static void tearDown() { /* <9.> */ }

    @AfterAll
    static void tearDownOnce() { /* <10.> */ }
}

測試的生命週期中有幾個步驟 (依數字參考範例註解中的位置):

  1. 檢查是否應該執行測試類別 (JUnit 稱之為容器) 中的測試
  2. 建立容器的實體
  3. 檢查是否要執行各別測試 (從生命週期角度看是這時候檢查,但從程式撰寫的角度看,在先前的步驟已經完成)
  4. 如果是要執行容器的第一個測試,先呼叫 @BeforeAll (以前是 @BeforeClass) 加註的函式
  5. 呼叫 @BeforeEach (以前是 @Before) 加註的函式
  6. 解析測試函式所需的參數 (現在測試函式可以有參數)
  7. 執行測試
  8. 處理可能拋出的例外
  9. 呼叫 @AfterEach (以前是 @After) 加註的函式
  10. 如果該測試是容器中最後一個測試,呼叫 @AfterAll (以前是 @AfterClass) 加註的函式。現在我們看怎麼與這樣的生命週期互動。

擴充點

當 JUnit 5 專案在 2015 起草時,主要的設計師們決定幾個核心原則,其中之一是:擴充點優先於新功能,如字面的意思,JUnit 5 提供擴充點,因此當一個測試經過剛剛所述生命週期的步驟時,JUnit 會暫停在定義好的擴充點,檢查是否有擴充程式想在特定的步驟與正在執行的測試互動,以下是擴充點的列表:

  • ContainerExecutionCondition
  • TestInstancePostProcessor
  • TextExecutionCondition
  • BeforeAllCallback
  • BeforeEachCallback
  • BeforeTestExecutionCallback
  • ParameterResolver
  • TestExecutionExceptionHandler
  • AfterTestExecutionCallback
  • AfterEachCallback
  • AfterAllCallback

注意到它們與測試的生命週期息息相關,在這之中,只有 BeforeTestExecutionCallbackAfterTestExecutionCallback 是新的,它們來自技術性的需求,想盡可能貼近測試,例如量測一個測試。

什麼是擴充,以及它如何與擴充點互動?每個擴充點都有一個相同名稱的 Java 介面,這些介面都相當簡單,通常只有一個或兩個(偶爾) 函式,在每個擴充點,JUnit 收集大量的情境資訊 (我待會提到),存取已註冊並實作對應介面的擴充,呼叫其函式,根據其回傳值改變測試的行為。

簡單的量測

在開始深入之前,我們先看一個簡單的例子,比如說我想量測我的測試,將消耗時間列印在終端機上。如您預期的,在測試開始執行前記錄測試開始的時間,在執行後列印消耗的時間。

看一下剛剛的擴充點列表,有兩個擴充點顯然能用:BeforeTestExecutionCallbackAfterTestExecutionCallback,它們的定義如下:

public interface BeforeTestExecutionCallback extends Extension {

    void beforeTestExecution(TestExtensionContext context) throws Exception;
}

public interface AfterTestExecutionCallback extends Extension {

    void afterTestExecution(TestExtensionContext context) throws Exception;
}

擴充的程式看起來像這樣:

public class BenchmarkExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private long launchTime;

    @Override
    public void beforeTestExecution(TestExtensionContext context) {
        launchTime = System.currentTimeMillis();
    }

    @Override
    public void afterTestExecution(TestExtensionContext context) {
        long elapsedTime = System.currentTimeMillis() - launchTime;
        System.out.printf("Test took %d ms.%n", elapsedTime);
    }
}

註冊擴充

這樣其實不足以實作一個擴充,JUnit 也知道這一點,一個擴充可以用 @ExtendWith 註冊,可以加註在類別或函式上,並將擴充的類別當作參數,執行測試的期間,JUnit 會尋找這些在類別或函式上的註釋,然後執行所有找到的擴充。

在一個容器或是函式上註冊擴充是等冪的,也就是說在相同元件上註冊相同的擴充多少次效果都是一樣的,那在不同的元件上註冊相同的擴充呢?

擴充是繼承的,這意味著一個函式會繼承套用在容器上的所有擴充,一個類別會繼承所有其超類別的擴充,它們是由外而內套用的,例如,一個註冊在容器上的 before-each 擴充會比註冊在函式上相同擴充點的擴充先執行。

與由上而下的策略相比,由外而內的策略,意指擴充在 “after” 的行為會以相反的順序執行,也就是註冊在函式上的擴充會比註冊在對應容器的擴充先執行 [譯註:before container -> before method -> after method -> after container]。

在相同的擴充點註冊不同的擴充,當然是可行的,它們套用的方式依舊是由外而內的,如同他們宣告的順序。

註冊一個量測的擴充 有了相關的認識後,我們套用一個量測的擴充:

// this is the way all methods are benchmarked

@ExtendWith(BenchmarkExtension.class)
class BenchmarkedTest {

    @Test
    void benchmarked() throws InterruptedException {
        Thread.sleep(100);
    }
}

在容器上註冊這擴充後,JUnit 套用這擴充到所有容器中的測試,並執行 benchmarked 函式,會看到測試大概花 100 ms。

如果您再一次在另一個函式註冊相同的擴充會發生什麼事?

@Test
@ExtendWith(BenchmarkExtension.class)
void benchmarkedTwice() throws InterruptedException {
    Thread.sleep(100);
    assertTrue(true);
}

根據稍早的解釋,這擴充將會再被套用,因此,您會看到二次量測的輸出。

解析擴充 讓我們感受一下註冊是如何實作的,當一個測試節點 (可能是一個容器或一個函式) 準備執行時,在萃取出真正的擴充類別前,JUnit 取得包覆節點 (類別或函式) 的 AnnotatedElement,用反射 (reflection) 的方式存取 @ExtendWith 註釋。

感謝便利的工具函式與 stream,讓 JUnit 用不顯著的程式片段便完成了這件事。

List<Class<? extends Extension>> extensionTypes =
    findRepeatableAnnotations(annotatedElement, ExtendWith.class)
        .stream()
        .map(ExtendWith::value)
        .flatMap(Arrays::stream)
        .collect(toList());

回傳的 List 用來建立 ExtensionRegistry,將 list 轉換成一個 set 確保等冪性,這註冊表不只知道擴充在哪個元件 (例如函式) 上,還有握有一個參考指向上層節點的註冊表,當擴充請求一個註冊表時,它存取自己的上層註冊表,並將其擴充套用在其結果中 [譯註:註冊表],上層註冊表同樣也會呼叫上層註冊表。

為實作我剛描述的由外而內的語意,ExtensionRegistry 提供二個函式:getExtensionsgetReversedExtensions,前者列出在自己之前上層有的擴充,因此,適合用先前提到的 “before” 順序,後者單純將前者的順序反轉,因此用在 “after” 的使用情境上。

無縫的擴充

@ExtendWith 套用擴充是可行,但太過技術性與麻煩,幸運地,JUnit 團隊也是如此認為,於是他們實作一個簡單卻有強大結果的功能,輔助函式尋找 超註釋 (meta-annotations),也就是套用在別的註釋上的註釋。

這意思是不一定要用 @ExtendWith 加註在一個型別或函式上,不論是直接,或是用同樣的方式間接加註 @ExtendWith 就足夠了,這對可讀性有重要的好處,讓您能寫出與函式庫特性無縫整合的擴充,我們來看二個使用案例。

無縫的量測 為量測的擴充建立一個更優雅的變形相當容易:

@Target({ TYPE, METHOD, ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(BenchmarkExtension.class)
public @interface Benchmark { }

感謝 Java 的 ElementType@Target 的規範,我可以像其他註釋一樣,套用 @Benchmark 在測試容器與函式上,也可以進一步的組合,這讓我重寫先前的例子讓它看起來更加親切:

@Benchmark
class BenchmarkedTest {

    @Test
    void benchmarked() throws InterruptedException {
        Thread.sleep(100);
    }
}

注意看一下是否更簡單了。

組合功能與擴充 另一個 JUnit meta-annotations 開啟的實用樣式 (pattern) 是能將既有的功能與擴充組合成新的、能揭示意圖的註釋,一個簡單的例子是 IntegrationTest [譯註:原文的註釋名稱 Benchmark 應該是誤植]:

@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Benchmark
@Tag("integration")
@Test
public @interface IntegrationTest { }

這是一個客制的註釋,一個專案可以建立來滿足整合測試的共同需求,在這情況下,所有這類加註 @Test 的測試會被量測並加上 integration 標籤,讓它們可以被過濾出來,更重要的是,可以讓您用 @IntegrationTest 取代 @Test

class ServerTest {

    @IntegrationTest
    void testLogin {
        // long running test

        Thread.sleep(10000);
    }
}

擴充情境

擴充模型中的一個基石是 ExtensionContext,是一個介面,有二個實作:ContainerExtensionContextTestExtensionContext,這讓擴充能獲得容器或測試目前狀態相關的資訊,它也提供一些 API 與 JUnit 機制互動,看一下它提供哪些函式:

Optional<ExtensionContext> getParent();
String getUniqueId();
String getDisplayName();
Set<String> getTags();
Optional<AnnotatedElement> getElement();
Optional<Class<?>> getTestClass();
Optional<Method> getTestMethod();
void publishReportEntry(Map<String, String> map);
Store getStore();
Store getStore(Namespace namespace);

JUnit 為測試節點建立一個樹狀結構,每個節點產生各自的情境,由於節點有上層節點 (例如,一個測試類別對應的節點,是測試類別中函式對應節點的上層節點),這讓它們的擴充情境參考到上層的情境。

為了讓您能識別與過濾容器與測試,這些項目都有 ID、更可讀的顯示名稱及標籤,能用情境物件的函式存取這些項目,非常重要的是,這些情境提供測試類別與函式的存取方式,這讓擴充能用反射 API 取得測試的註釋或類別的欄位,讓我們用實例來了解這特性:強化量測的擴充可以在日誌訊息中顯示測試的顯示名稱:

@Override
public void afterTestExecution(TestExtensionContext context) {
    long elapsedTime = System.currentTimeMillis() - launchTime;
    System.out.printf("Test '%s' took %d ms.%n", context.getDisplayName(), elapsedTime);
}

您可以更進一步,與其粗略地列印在終端機上,您可以呼叫 publishReportEntry 函式使用 JUnit 的報表基礎建設:

@Override
public void afterTestExecution(TestExtensionContext context) {
    long elapsedTime = System.currentTimeMillis() - launchTime;
    String message = String.format("Test '%s' took %d ms.", context.getDisplayName(), elapsedTime);
    context.publishReportEntry(createMapWithPair("Benchmark", message));
}

我不會深入討論 JUnit 報表功能,但足以說它能紀錄訊息到不同的輸出,像是終端機或 XML 報表,publishReportEntry 函式讓擴充能與報表互動,最後,有一個資料容器用來保存擴充的狀態,我很快會談到。

如同我剛提到的,JUnit 負責尋找與套用擴充,這也意味它也同樣管理擴充的實體,它是如何做到?如果您想將收集到的資訊指派到欄位,如我剛在 BenchmarkExtension 中做的,您需要瞭解擴充的範圍與生命週期。

就結果來說,那是刻意未規範的實作細節,定義擴充實體的生命週期或在測試過程中追蹤它,不只是令人討厭的,從壞處想,對可維護性來說是一個威脅,所以一切都是未知數,JUnit 不保證關於擴充實體生命週期的任何事,因此,擴充必須是無狀態的,並將任何資訊儲存在 JUnit 為這目的提供的資料結構中,稱為 保存庫 (store)

保存庫 是一個有命名空間、階層式的 key-value 資料結構,我們各別來看這些屬性。

要透過擴充情境存取保存庫,一個命名空間是需要的,情境回傳一個保存庫,管理這命名空間下的所有項目,這麼做能避免不同擴充在操作相同節點時的衝突,可能導致意外的資訊分享與狀態修改 (有趣的是,透過命名空間存取可以刻意地去存取另一個擴充的狀態,能和另一個擴充溝通與互動,可能促成跨函式庫的有趣功能)。

因為是為每一個擴充情境建立保存庫,所以保存庫是階層式的,這意味測試樹狀結構中每個節點都有一個保存庫。每個測試容器與函式都有自己的保存庫,如同節點繼承擴充一樣,保存庫繼承狀態,更確切地說,當一個節點建立一個保存庫,這節點將指向上層節點保存庫的參考交給該保存庫,因此,例如一個屬於測試函式的保存庫,有一個參考指向所屬測試類別的保存庫,查詢時 (不含編輯),在委託給上層的保存庫前,保存庫先檢查自己擁有的內容,這讓自己所有子保存庫能讀取自己的狀態。

因為是為每一個擴充情境建立保存庫,所以保存庫是階層式的,這意味測試樹狀結構中每個節點都有一個保存庫。

關於 key-value 資料結構,保存庫是一個簡化過的 map,key 和 value 可以是任何型別,以下是最不可或缺的函式:

interface Store {

    void put(Object key, Object value);
    <V> V get(Object key, Class<V> requiredType);
    <V> V remove(Object key, Class<V> requiredType);
}

getremove 函式用型別參數避免呼叫者的程式充滿轉型,其實這沒什麼魔術,保存庫只是單純在內部做轉型,同樣也有不需要型別參數的多載函式。

無狀態的量測 要讓量測是無狀態的,我需要幾件事:

  • 一個能讓擴充存取保存庫的命名空間
  • 一個識別啟動時間的 key
  • 能讀寫保存庫而不是欄位的能力

為了前二個,我宣告二個常數:

private static final Namespace NAMESPACE = Namespace.create("org", "codefx", "Benchmark");
private static final String LAUNCH_TIME_KEY = "LaunchTime";

讀寫是二個簡單的函式:

private static void storeNowAsLaunchTime(ExtensionContext context) {
    context.getStore(NAMESPACE).put(LAUNCH_TIME_KEY, currentTimeMillis());
}

private static long loadLaunchTime(ExtensionContext context) {
    return context.getStore(NAMESPACE).get(LAUNCH_TIME_KEY, long.class);
}

有了這些函式,我將可以移除的存取欄位 launchTime 的程式取代,在每個測試前後執行的函式變成:

@Override
public void beforeTestExecution(TestExtensionContext context) {
    storeNowAsLaunchTime(context);
}

@Override
public void afterTestExecution(TestExtensionContext context) {
    long launchTime = loadLaunchTime(context);
    long runtime = currentTimeMillis() - launchTime;
    print(context.getDisplayName(), runtime);
}

如您所見,新的函式用保存庫取代欄位來保存與存取情境的狀態。

翻新 @Test

我們來看能善用我先前提到的素材的新例子。

假設我想從 JUnit 4 轉到 JUnit 5,首先,感謝新架構的設計,同時執行新版與舊版的測試相當簡單,這意指不必特別去轉移測試,這雖然讓下面的內容毫無意義,但也少了點樂趣。

我想將 JUnit 4 的 @Test 換成新版的,讓加註的函式變成 JUnit 5 的測試,我會換成 JUnit 5 的 @Test,一個簡單的尋找取代 import 就可以完成,這方法可以用在太部分的情況。(注意:這只是一個想法實驗,不是實際的建議)

但 JUnit 5 的註釋不支援 JUnit 4 選擇性的參數 expected (當特定例外沒有拋出時視為失敗) 以及 timeout (當測試執行太久視作失敗),JUnit 5 透過 assertThrows 及即將推出的 assertTimeout 提供這些功能,但我想找一個不用手動介入的新方式,不用將測試升級到新的 API。

同時執行新版與舊版的測試相當簡單,這意指不必特別去轉移測試。

所以為什麼不建立我自己的 @Test,讓 JUnit 5 能夠識別並執行,且實作想要的功能呢?

最重要的事情優先,我宣告一個新的 @Test 註釋:

@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
@org.junit.jupiter.api.Test
public @interface Test { }

這相當簡單:我只是宣告註釋,並在註釋上加上 JUnit 5 的 @Test,因此 JUnit 能辨別出加註的函式是個測試並執行它們。

期待例外 為管理預期中的例外,首先我需一個方法讓使用者能宣告它們,為此,我用從 JUnit 4 的實作中獲得靈感的程式擴充我的註釋:

public @interface Test {

    class None extends Throwable {

        private static final long serialVersionUID = 1L;

        private None() { }
    }

    Class<? extends Throwable> expected() default None.class;
}

現在,使用者可以用 expected 指定預期的例外,預設是 None。擴充本身是一個稱作 ExpectedExceptionExtension 的類別,程式碼顯示於下方,要將它註冊到 JUnit,我可以加註 @Test@ExtendWith(ExpectedExceptionExtension.class)

接著,我需要實際實作想要的行為,這裡簡單描述我要如何完成它:

  1. 如果有測試拋出一個例外,檢查它是否是預期中的例外,若是則將它確實拋出的事實記錄下來,否則將不如預期的錯誤紀錄下來,並將攔截下來的例外拋出 (因為擴充不負責處理例外)。
  2. 測試執行後,檢查預期的例外是否拋出,如果是,則什麼事都沒發生因為事情和計劃的一樣,若不是則讓測試失敗。

為了完成這邏輯,我需要與二個擴充點互動:TestExecutionExceptionHandlerAfterTestExecutionCallback,因此我實作對應的介面:

public class ExpectedExceptionExtension implements TestExecutionExceptionHandler, AfterTestExecutionCallback {

    @Override
    public void handleTestExecutionException(TestExtensionContext context, Throwable throwable) throws Throwable { }

    @Override
    public void afterTestExecution(TestExtensionContext context) { }
}

開始第一步,檢查拋出的例外是否如預期,為此,我使用一個小的輔助函式 expectedException,存取 @Test 註釋,取出預期的例外類別,回傳結果是一個 Optional (因為可能沒有預期的例外)。

為了捕捉觀察到的行為,我建立一個列舉 EXCEPTION,並寫了一個 storeExceptionStatus 以保存觀察到的結果在保存庫中,有了這些輔助,我可以實作第一個擴充點:

@Override
public void handleTestExecutionException(TestExtensionContext context, Throwable throwable) throws Throwable {
    boolean throwableMatchesExpectedException = expectedException(context)
        .filter(expected -> expected.isInstance(throwable))
        .isPresent();
    if (throwableMatchesExpectedException) {
        storeExceptionStatus(context, EXCEPTION.WAS_THROWN_AS_EXPECTED);
    } else {
        storeExceptionStatus(context, EXCEPTION.WAS_THROWN_NOT_AS_EXPECTED);
        throw throwable;
    }
}

注意,透過不拋出例外,我告知 JUnit 我已經處理它且一切正常,因此 JUnit 不會呼叫額外的例外處理器,也不會讓測試失敗,到目前為止一切順利。

現在,當測試執行後,我需要檢查發生什麼事以做對應的處理,另一個輔助函式 loadExceptionStatus 將取出狀態並進一步幫我一點小忙:若沒有例外拋出,我剛實作的擴充點不會被執行,這意味沒有 EXCEPTION 的實體放到保存庫中,在這情況,loadExceptionStatus 會回傳 EXCEPTION.WAS_NOT_THROWN,實作如下:

@Override
public void afterTestExecution(TestExtensionContext context) {
    switch(loadExceptionStatus(context)) {
    case WAS_NOT_THROWN:
        expectedException(context)
            .map(expected -> new IllegalStateException("Expected exception " + expected + " was not thrown."))
            .ifPresent(ex -> { throw ex; });
    case WAS_THROWN_AS_EXPECTED:
        // the exception was thrown as expected, 

        // so there is nothing to do

    case WAS_THROWN_NOT_AS_EXPECTED:
        // an exception was thrown but of the

        // wrong type; it was rethrown in

        // handleTestExecutionException,

        // so there is nothing to do here

    }
}

這方法有二個細節值得討論:

  • 是否有比 IllegalStateException 更合適的例外?例如,AssertionFailedError 也許更好。
  • 如果有非預期的例外拋出,我是否應該讓測試當下就視作失敗?

我在 handleTestExecutionException 中重新拋出捕捉到的例外,所以它可能讓測試失敗,或被其他能讓測試通過的擴充給捕捉,所以讓測試就地失敗,可能破壞其他擴充。

這二個議題都值得在未來繼續完成,但除此之外,我們已經完成能捕捉預期例外的擴充。

逾時 原本的逾時設計保證 JUnit 4 會在指定的時間用完時中斷測試,這需要將測試丟到另一個獨立的執行緒執行,不幸地,JUnit 5 沒有擴充點能處理執行緒,所以這不可能。悲慘的結果是沒有任何擴充能讓測試在特定的執行緒上執行,像是 Swing 測試需要的事件派送執行緒,或是 JavaFX 測試需要的應用程式執行緒,JUnit 團隊已充分理解這限制,希望他們能盡快處理。

您可以實作一個替代的版本,量測測試執行花多少時間,這暗示測試必須要能結束,當時間超過指定門檻時讓測試失敗,如果前面所述,這應該相當容易。

Conclusion

我們已經理解 JUnit 5 提供特定的擴充點,就是一些界面,擴充的開發者可以實作這些介面,直接用 @ExtendWith 註冊他們的實作,或是無縫地使用客制的註釋。

在測試的生命週期中,JUnit 會在每個擴充點暫停,尋找可以套用在目前測試節點的擴充,收集情境資訊,由外而內的順序呼叫擴充,擴充操作情境以及他們儲存在保存庫中的任何狀態,JUnit 根據呼叫函式的回傳值做出反應,並改改變測試的行為。

我們同樣看到您可以如何用這些完成一個簡單的量測擴充,以及更進一步,完成 JUnit 4 @Test 註釋的複刻,您能在我的 GitHub 找到這些及其他更多的例子。

如果您有任何問題或評論想分享,請讓我知道。

在測試的生命週期中,JUnit 會在每個擴充點暫停,尋找可以套用在目前測試節點的擴充,收集情境資訊,由外而內的順序呼叫擴充。

譯者的告白
這一篇文章很長,而且代名詞與子句超多,看是看得懂,但要翻成通順的中文卻很難。話說,我為什麼要在三連假翻譯和寫文章啊~完全沒有放鬆的感覺,嗚~ (友藏內心獨白:反正你也是一個人過節不是嗎?)

 
over 1 year ago

自殺產業

這周回到家整個都懶懶的,只翻譯了一點點,所以這周原先預定的 JUnit 5 翻譯確定跳票了。不過自己有興趣的想法算是整理完了,和幾個人討論過後,似乎消費者並沒有這麼在意那件事,或許沒有合適的市場吧?自己在軟體產業,有時也會想怎麼幫助這個產業?或是怎麼去幫助傳統產業升級?但仔細想想資訊發展的本質是盡可能讓所有的東西自動化,與傳統產業結合,是否是迫使原本在傳統產業的人失業呢?

於是就想到當初唸研究所時指導教授就曾說過:軟體工程師就是讓自己失業的職業,我們讓自己原先的工作自動化,於是這個工作就不需要人了,但身為工程師我們總是能找到下個可以自動化的領域,短時間其實還不用太擔心失業,但傳統產業的人呢?他們是被迫失業,又不像我們可以找下個地方繼續自動化,這樣真的比較好嗎?如果這樣想,軟體工程師不只是革自己的命也是革別人的命?

也許短期內可能不用擔心失業,碰巧又最近在 Facebook 上看到台大洪教授的文章,就在想:隨著 AI 持續的進步,也許哪天出現寫程式的 AI,一些瑣碎不難的程式都交給 AI 後,技藝不精的軟體工程師大概也就失業了,而僅僅剩下那種超難超複雜的程式需要人去開發,但這需要的是超級厲害的工程師,希望那一天不會這麼早到來啊...

Agile Tour Taipei 2016

離開先前待了三年的 Agile 團隊後,本來是想和朋友一起在新團隊再次用 Agile 的思維開發軟體,不過在被併之後,自己像是得了傳統專案管理適應不良症候群,不知道自己能撐到什麼時候?

還好,周六的 Agile Tour Taipei 2016 讓我覺得還是有不少公司識貨 (好啦~也有不少公司還是在撞牆期),但至少讓我看到希望,和當初畢業時相比,Agile 社群中的人變多了,採用的公司也開始多起來,能聽到 Ruddy 老師精彩的演講很棒,學到引導的技巧也很棒,下午的 UX in the Jungle 桌遊不但有趣也可以體會到 UX、開發與市場之間的關係,雖然我們組沒拿到最高分,但大家都玩的很開心,晚場的 open space 發現我也能有不少經驗能分享給別人,也從別人那裡吸收到不少東西,算是把快乾涸的 Agile 信心補充了一點回來。

結束後和前同事閒聊,前團隊 run Agile 已經三、四年,也做了多次轉變和進化,雖然聽起來這次的轉變有點微妙,還是祝福他們是進化而不是退化?

最近會找點東西來看、翻譯,或做些分享與充電,大概是想填補工作內容極度無趣的空虛感吧?真希望那群寫爛程式、對自己的爛程式毫無感覺的人能趕快被取代掉,至於產品本身...自己個人是完全不想用。

 
over 1 year ago

新的個人看板

用 Trello 管理自己的一些事情有一陣子了,包含翻譯 Java Magazine 和一些亂七八糟的發想,搭配番茄鐘用起來還不錯,但最近發現,天啦!我在 Trello 上有好幾個看板,為了追蹤正在做什麼事,我得在好幾個看板中切換,所以,這次把過去建的看板都拿掉了,只剩下一個個人看板,不同類型的工作項目,改成用不同顏色的標籤標註,細項的 Task 則用 Trello 卡片的待辦清單表示,有些有加上期望的截止時間。

這次看板分成 New Idea,放一些就真的還只是 idea 的東西;Backlog,放確定要做的事情;Prepare,算是已經開始進行,不過是做一些準備工作,像是收集資料等;In progress,則是進行真正重要的內容;Done,不用多說,就是完成的項目。

以剛出爐的《Part 2: 使用 JUnit 5》為例,被切成三個 tasks:複製原文 (到 logdown)、翻譯和校對,開始複製原文時,我會把卡片移到 Prepare,開始翻譯時,會把卡片移到 In Progress,校對完成後就會移到 Done,這次試行的效果,到目前為止,覺得還不錯,可以觀察一陣子再看要不要調整。

話說,這看板少了一樣東西,不知道有人發現了嗎?

為什麼要翻譯 Java Magazine

最近發現我看動畫的時間變少了,相較之下前幾個月動畫看蠻兇的,主要是開始寫閒談軟體架構和翻譯喜歡的 Java Magazine 佔去不少時間,但為什麼要做這些事呢?我想了一下,當初開始翻譯 Java Magazine 是因為裡面某些文章我蠻喜歡的,所以我就寄信問 Java Magazine 的主編,是的,我真的寄信給主編,然後主編也真的回信了

Thank you for your patience. We do allow translation of the articles in Java Magazine.

Oracle has the following requirements, which I expect you will find fairly standard.

The translation needs to be faithful to the original. It must state the original article's title, that it appeared in Java Magazine, including the date of the issue and the page number. It should also state Oracle's copyright. Finally, the article should have a link back to our home page, http://www.oracle.com/technetwork/java/javamagazine/index.html

For example, if you were to translate the article on Jython in the current issue, we'd expect to see this:

Translated from "Jython 2.7 Integrating Python and Java" by Jim Baker and Josh Juneau, Java Magazine , November/December 2015, page 43. [link here] Copyright Oracle Corporation.

If you keep me informed when you post translations, we will let our readers know.

Please let me know if you have any questions now or in the future.

Again, thank you for your interest in Java Magazine. I look forward to seeing the articles in Chinese!

Andrew Binstock

從那時候開始,只要有喜歡的文章就會翻譯,沒興趣的就隨風去吧,翻了一陣子後,相較於另一本線上雜誌 DNC Magazine,每一期 Java Magazine 的每篇文章不管喜不喜歡都有看完,也讓我學到蠻多新東西的,變成一種敦促自己學東西的方法。

最近,在 google 技術知識時,發現簡體中文的比例越來越高了,雖然本來就不期待會找到正體中文相關的文章,但總覺得台灣在技術分享這件事上,好像沒有很熱烈,讓我想到上周參加老闆學校講座最終場《2016年, 老闆,我想做遊戲可以嗎?》的會後,也聽到一位前遊戲業界人士與講者的談話,在 DOS 和早期 Windows 時代時,台灣遊戲公司就那麼幾家,彼此之間保密防諜,沒什麼技術交流,相關人才的數量自然就不會成長,他覺得像這樣的分享很好。這大概也是讓我覺得翻譯 Java Magazine 這件事,除了讓自己的進步外,對正體中文的技術類分享能多一點幫助,即使只有一點點也好。

最後,周五是動畫夜,不會安排任何事,要把本周追的新番都看完。 (OS:加到個人看板中如何?)