通過行為引數化傳遞程式碼

Philip發表於2018-08-31

初試牛刀:篩選綠蘋果

第一個解決方案可能是下面這樣的:

public static List<Apple> filterGreenApples(List<Apple> inventory) {
                                    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory) {
        if("green".equals(apple.getColor())) {// 篩選蘋果的條件
            result.add(apple);
        }
    }
    return result;
}
複製程式碼

但是現在還想要篩選紅蘋果,你該怎麼做呢?簡單的解決方法就是複製這個方法,把名字改為filterRedApple,然後更改if條件來匹配紅蘋果。然後,要是還想篩選多種顏色:淺綠色、暗紅色、黃色等,這種方法就應付不了了。一個良好的原則是在編寫類似的程式碼之後,嘗試將其抽象化。

再展身手:把顏色作為引數

一種做法是給方法加一個引數,把顏色變成引數,這樣就能靈活地適應變化了:

public static List<Apple> filterApplesByColor(List<Apple> inventory, 
                                                String color) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory) {
        if(apple.getColor().equals(color)) {// 篩選蘋果的條件
            result.add(apple);
        }
    }
    return result;
}
複製程式碼

但是現在還想篩選重的蘋果(質量大於150g的蘋果),於是你寫了下面的方法,用另一個引數來應對不同的重量:

public static List<Apple> filterApplesByWeight(List<Apple> inventory, 
                                            int weight) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory) {
        if(apple.getWeight() > weight) {// 篩選蘋果的條件
            result.add(apple);
        }
    }
    return result;
}
複製程式碼

解決方案不錯,但是請注意,你複製了大部分的程式碼來實現便利庫存,並對每個蘋果應用篩選條件。這有點兒令人失望,因為它打破了DRY(Don't Repeat Yourself,不要重複自己)的軟體工程原則。如果你想要改變篩選遍歷方式來提升效能呢?那就得修改所有方法得實現,而不是隻改一個。從工程工作量的角度來看,這代價太大了。 你可以將顏色和重量結合為一個方法,成為filter。不過就算這樣,你還需要一種方式來區分想要篩選哪個屬性。

第三次嘗試:對你能想到的每個屬性做篩選

一種把所有屬性結合起來的笨拙嘗試如下所示:

public static List<Apple> filterApples(List<Apple> inventory, 
                            String color, int weight, boolean flag) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory) {
        if((flag && apple.getColor().equals(color)) ||
        (!flag && apple.getWeight > weight)) {// 篩選蘋果的條件
            result.add(apple);
        }
    }
    return result;
}
複製程式碼

你可以這麼用(但真的很笨拙)

List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
複製程式碼

這個解決方案在差不過了。首先,客戶端程式碼看上去糟透了。true和false是什麼意思?此外,這個解決方案不能很好地應對變化的需求。如果要求你對蘋果的不同屬性做篩選,比如大小,形狀,產地等,又怎麼辦?而且,如果要求組合屬性篩選,做更復雜的查詢,比如綠色的重蘋果,又該怎麼辦?但如今這種情況下,你需要一種更好的方式,來把蘋果的選擇標準告訴你得filterApples方法。 讓我們後退一步來看看更高層次的抽象。一種可能的解決方案是對你的選擇標準建模:你考慮的是蘋果,需要根據Apple的某些屬性來返回一個Boolean值。我們把它稱為謂詞(即一個返回Boolean值的函式)。讓我們定義一個介面來對選擇標準建模:

public interface ApplePredicate {
    boolean test(Apple apple);
}
複製程式碼

現在你就可以用ApplePredicate的多個實現代表不同的選擇標準了,比如:

// 篩選重量大於150的蘋果的謂詞
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
// 篩選顏色為綠色的蘋果的謂詞
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}
複製程式碼

但是,該怎麼利用ApplePredicate呢?你需要filterApples方法接受ApplePredicate物件,對Apple做條件測試。這就是行為引數化:讓方法接受多種行為(或策略)作為引數,並在內部使用,來完成不同的行為。

第四次嘗試:根據抽象條件篩選

利用ApplePredicate改過之後,filter方法看起來是這樣的:

public static List<Apple> filterApples(List<Apple> inventory, 
                                            ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory) {
        if(p.test(apple)) {
            result.add(apple);
        }	
    }
    return result;
}
複製程式碼

你已經做成了一件很酷的事:filterApples方法取決於你通過ApplePredicate物件傳遞的程式碼。換句話說,你把filterApples方法的行為引數化了! 請注意,在上一個例子中,唯一重要的程式碼事test方法的實現,正是它定義了filterApples方法的新行為。但令人遺憾的是,由於該filterApples方法只能接受物件,所以你必須把程式碼包裹在ApplePredicate物件裡。你的做法就類似於在內聯”傳遞程式碼“,因為你是通過一個實現了test方法的物件來傳遞布林表示式的。

public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
複製程式碼

把策略傳遞給策略方法:通過不二表示式篩選封裝在ApplePredicate物件內的蘋果。為了封裝這段程式碼,用了很多模板程式碼來包裹它(粗體顯示)

我們都知道,人們都不願意用那些很麻煩的功能或概念。目前,當要把新的行為傳遞給filterApples方法的時候,你不得不宣告好幾個實現ApplePredicate介面的類,然後例項化好幾個只會提到一次的ApplePredicate物件。下面的程式總結了你目前看到的一切。這真是很囉嗦,很費時間。 費那麼大勁兒真沒必要,能不能做的更好呢?Java有一個機制稱為匿名類,它可以讓你同時宣告和例項化一個類。它可以幫助你進一步改善程式碼,讓他變得更簡潔。但這也不完全令人滿意。 匿名類:匿名類和你熟悉的Java區域性類(塊中定義的類)差不多,但匿名類沒有名字。它允許你同時宣告並例項化一個類。換句話說,它允許你隨用隨建。

第五次嘗試:使用匿名類

下面的程式碼展示瞭如何通過建立一個匿名類實現ApplePredicate的物件,重寫篩選的例子:

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    }
})
複製程式碼

GUI應用程式中經常使用匿名類來建立事件處理器物件(下面的例子使用的是Java FX API,一種現代的Java UI平臺)

buuton.setOnAction(new EventHandler<ActionEvent>() {
    public void handler(ActionEvent event) {
        System.out.println("Woooo a click!");
    }
});
複製程式碼

但匿名類還是不夠好。第一,它往往很笨重,因為它佔用了很多空間。還拿前面的例子來看,如下面加粗程式碼所示:

List<Apple> redApples = filterApples(inventory, new ApplePredicate(){
    public boolean test(Apple a){
        return "red".equals(a.getColor());
    }
});
buuton.setOnAction(new EventHandler<ActionEvent>() {
    public void handler(ActionEvent event) {
        System.out.println("Woooo a click!");
    }
});
複製程式碼

第二,很多程式設計師覺得它用起來很讓人費解

第六次嘗試:使用Lambda表示式

上面的程式碼在Java8裡可以用Lambda表示式重寫為下面的樣子:

List<Apple> result = 
    filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
複製程式碼

不得不承認這程式碼看上去比先前乾淨很多。這很好,因為它看起來更像問題陳述本身了。我們已經解決了囉嗦的問題。

第七次嘗試:將List型別抽象化

在通往抽象的路上,我們還可以更進一步。目前,filterApples方法還只適用於Apple。你還可以將List型別抽象化,從而超越你眼前要處理的問題:

public interface Predicate<T>{
    boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
                                List<T> result = new ArrayList<>();
    for(T e : list){
        if(p.test(e)){
            result.add(e);
        }
    }
    return result;
}
複製程式碼

你現在已經看到,行為引數化是一個很有用的模式,它能夠輕鬆地適應不斷變化的需求。這種模式可以把一個行為(一段程式碼)封裝起來,並通過傳遞和使用建立的行為(例如對Apple的不同謂詞)將方法的行為引數化。前面提到過,這種做法類似於策略設計模式。你可能已經在實踐中用過這個模式了。Java API中的很多方法都可以用不同的行為來引數化。這種方法往往與匿名類一起使用。我們會展示三個例子,這應該能幫助你鞏固傳遞程式碼的思想:用一個Comparator排序,用Runnable執行一個程式碼塊,以及GUI事件處理。

相關文章