宣告式語意的脈絡

這個網誌發表過不少宣告式語意的文章,它們是同人以泛函編程的思維,以不同語言包括 C++、以及 Java 1.6 實作宣告式語意的手法。然而,這半年來,同人用 Java 1.8 實現宣告式語意的設計手法,深深感受到宣告式語意脈絡之妙,所以這篇文章要來談一談宣告式語意的脈絡,讓我們姑且稱它為 Context 樣式吧!

宣告式的運算邏輯和命令式的運算邏輯的差別,用最簡單實際而又具體的語言來說明,就是前者用「條件判斷」和「迴圈處理」來控制流程,而後者則是取法於數學模型典範;也就是利用操作「集合」的概念並且運用「遞迴」的手法來化簡問題,還有用函式表示式來「延遲運算」,使宣告式語意變為可行。如同下面宣告式語意的的簡單範例:

List<MyResult> result = myList.stream()
    .filter(item -> item.getScore() > 60)
    .map(item -> new MyResult(item))
    .collect(Collectors.toList());

上面這段程式碼利用 Lambda 表示式把篩選過的元素進行映射處理,再收集成新的 List 容器。Lambda 表示式是一種匿名的函式,上面的寫法看起來很簡單易懂,但真正的條件篩選和映射通常沒有那麼簡單,所以對比較複雜的篩選或映射邏輯,通常會寫成具名的函式。這樣寫會有兩個好處,一來是可讀性較高,再來就是會促使我們思考具體演算背後的抽象概念,而這些抽象概念可以化成企業元件提供其它程式重複使用以及增加系統彈性。所以程式碼就會演進成如下所示:

{

...

List<MyResult> result = myList.stream()
    .filter(this::isScoreGreatSixty)
    .map(this::toResult)
    .collect(Collectors.toList());

...

}

private boolean isScoreGreatSixty(MyItem item) {
    return item.getScore() > 60;
}

private MyResult toResult(MyItem item) {
    return new MyResult(item);
}

具名函式擁有和使用它的程式碼大半的脈絡,除了區域變數和呼叫其它元件的回傳值之外。在這種狀況下,我們就要用 Function Object,也就是在 C0x STL 稱為 Functor 的相同手法,如同上面的篩選器,我們將會改寫成下面的這個樣子:

{

...

List result = myList.stream()
    .filter(new ItemScoreGreatPredicate(60))
    .map(this::toResult)
    .collect(Collectors.toList());

...

}

class ItemScoreGreatPredicate implements Predicate<MyItem> {

    private Integer targetNumber;

    public ItemScoreGreatPredicate(Integer targetNumber) {
        this.targetNumber = targetNumber;
    }

    public boolean test(MyItem item) {
        return item.getScoreNumber) > targetNumber;
    }

}

想必這時你己經發現到,ItemScoreGreatPredicate 已經被參數化,代表它可以重覆使用在不同的使用情境下。但這也意味要共享呼叫者的脈絡,必須多花費一番功夫,已經不如 closure 般自由,然而這是相依性和獨立性的取捨。No side effects 是泛函編程的一個重要特性,但在宣告式語意中,脈絡則是延遲運算不可或缺的要素。

延遲運算是宣告式語意中,用來對治複雜性的利器。假如我們要解決的問題,並沒辦法單獨每一元素以 item -> toResult(item) 的方式解決,而是每一個元素的計算都會和其它元素扯上關係,而且因應不同的情境,會存在不同的演算策略的話,這時候就該輪到脈絡物件上場了,就像下面這一段程式一樣。

{

...
MyResultContext ctx = MyResultContext.create();

myList.stream()
    .filter(new ItemScoreGreatPredicate(60))
    .forEach(ctx::add);

List<MyResult> result = ctx.getResult(myResultPolicyFactory);
...

}

class MyResultPolicy implements Function<MyItem, MyResult> {
...

}

class MyResultContext {

private List<MyItem> items = new ArrayList<>();

public static MyResultContext create() {
    return new MyResultContext();
}

private MyResultContext {
}

public void add(MyItem item) {
    items.add(item);
}

public List<MyResult> getResult(
    ResultPolicyFactory policyFactory) {
    Function<MyItem, MyResult> policy = policyFactory.get(this);

    return items.stream().map(policy);
}

}

在這裡同人只是想簡單表達封裝策略演算法的觀念,所以在實際上,上面的實作多半要更複雜許多。從這個簡單範例可以發現 Context 樣式可以讓宣告式語意更有彈性,不過,對 GOF 設計樣式熟悉的人應該會發現,這不過是 BuilderStrategy 樣式的延伸,也再一次讓我們印證 FP 搭配 OOP 的強大威力。

Please follow and like us:
分類: 分析設計建模, 思考, 編程技巧, 職場, 設計原則, 軟體開發。這篇內容的永久連結

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *