over 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 表示式仍是一個很重要的特性!

← 本周雜記 (2016/1/5 ~ 2016/1/21)