通過行為參數化傳遞代碼
行為參數化是可以幫助你處理頻繁變更的需求的一種軟件開發模式。它意味著拿出一個代碼塊,將它準備好卻不去執行它,這個代碼塊可以被程序的其他部分調用。
將代碼塊作為參數傳遞給另一個方法,稍后再去執行它。這樣這個方法就基于那塊代碼被參數化了。打個比方,可能會這樣處理一個集合:
- 可以對列表中的每個元素做 “某件事”
- 可以再列表處理完后做 “另一件事”
- 遇到錯誤時可以做 “另外一件事”
這樣一個方法便可以接受不同的新行為作為參數去執行。
行為參數化
舉個栗子:假設要求你對蘋果的不同屬性做篩選,比如大小、形狀、產地、重量等,寫好多個重復的filter方法或者一個巨大的非常復雜的方法都不是好辦法。為此需要一種更好的方法,來把蘋果的選擇標準告訴filterApples方法。更高層次的抽象這個問題,對選擇標準進行建模:蘋果需要根據Apple的某些屬性來返回一個boolean值,這需要一個返回boolean值的函數,我們把它稱為謂詞。
現在我們定義一個接口來對選擇標準建模:
public interface ApplePredicate(){
boolean test(Apple apple);
}
現在就可以用ApplePredicate的多個實現代表不同的選擇標準了:
static class AppleWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
static class AppleColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
static class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
這些標準可以看做filter方法的不同行為。上面做的這些和“策略設計模式”相關,它讓你定義一族算法,把它們封裝起來,然后運行的時候選擇一個算法,這里的算法族就是applePredicate
,不同的策略就AppleHeavyWeightPredicate
和AppleGreenColorPredicate
。
下面的問題是,要怎么利用applePredicate的不同實現。需要讓filterApples
方法接受applePredicate
對象,對Apple做條件測試。這就是行為參數化:讓方法接受多種行為作為參數,并在內部使用,來完成不同的行為。
為此需要給filterApples方法添加一個參數,讓它能接受ApplePredicate對象。這樣在軟件工程上帶來的好處就是:將filterApples方法迭代集合的邏輯與你要應用到集合中每個也元素的行為(這里是謂詞)區分開了。
由此便可以修改ApplePredicate方法為:
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;
}
現在便可以創建不同的ApplePredicat
e對象,并將它們傳遞給filterApples
方法。filterApples
方法的行為取決于你通過ApplePredicate對象傳遞的代碼。實現了行為參數化。
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
List<Apple> redAndHeavyApples =
filterApples(inventory, new AppleRedAndHeavyPredicate());
對付啰嗦
雖然已經可以把行為抽象出來讓代碼適應需求的變化了,但是這個過程很啰嗦,因為當要把新的行為傳遞給方法的時候,可能需要聲明很多實現接口的類來實例化好幾個只要實例化一次的類(劃重點,這邊說的是對于只要實例化一次的類)。這樣又啰嗦又費時間。
對于這個問題也有對應的解決辦法,Java有一個機制稱為匿名類,它可以讓你同時聲明和實例化一個類,使代碼更為簡潔。
匿名類
匿名類和Java局部類差不多,但匿名類沒有名字。它允許你同時聲明并實例化一個類(隨用隨建)。
用匿名類實現的ApplePredicate對象:
List<Apple> redApples= filterApples(inventory, new ApplePredicate(){
public boolean test(Apple apple){
return "red".euqls(apple.getColor());
}
});
但是這也并不完全令人滿意。第一,它代碼占用很多行。第二,它用起來容易讓人費解。即使匿名類處理在某種程度上改善了為一個接口聲明好幾個實體類的啰嗦問題,讓仍不能讓人滿意。更好的方式是通過Lambda表達式讓代碼更易讀。
使用Lambda表達式
上面的代碼在Java8里可以用Lambda表達式重寫為下面的樣子:
List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(getColor()));
這代碼看上去比先前干凈很多。因為它看起來更像問題陳述本身了。
將List類型抽象化
目前的filterApples方法還只適用于Apple。還可以將List類型抽象化,從而超越眼前要處理的問題:
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> inventory, ApplePredicate<T> p){
List<T> result = new ArrayList<>();
for(T e : list ){
if(p.test(e)){
result.add(e);
}
}
return result;
}
現在可以把filter方法用在其他列表上了。
真實的例子
下面將通過兩個例子來鞏固傳遞代碼的思想。
用Comparator來排序
想要根據不同的屬性使用一種方法來表示和使用不同的排序行為來輕松地適應變化的需求。
在Java 8中,List自帶了一個sort方法(也可以使用Collections.sort)。sort的行為可以用java.util.Comparator對象來參數化,它的接口如下:
public interface Comparator<T>{
public int compare(T o1,T o2);
}
因此可以隨時創建Comparator的實現,用sort方法表現出不同的行為。
按照重量升序對庫存排序:
inventory.sort(new Comparator<Apple>(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
如果用Lambda表達式,它看起來會是這樣:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
(這段代碼并沒有告訴你如何同時遍歷列表中的兩個元素,不要糾結遍歷問題,下一章會詳細的講解如何編寫和使用Lambda表達式。)
用Runnable執行代碼塊
線程就像是輕量級的進程:它們自己執行一個代碼塊。多個線程可能會運行不同的代碼。為此需要一種方式來代表要執行的一段代碼。在Java里,可以使用Runnable
接口表示一個要執行的代碼塊:
public interface Runnabke{
public void run();
}
可以像下面這樣使用這個接口創建執行不同行為的線程:
Thread t = new Thread(new Runnable()){
public void run(){
System.out.println("Hello world");
}
});
用Lambda表達式的話,看起來會是這樣:
Thread t =new Thread(() -> System.out.println("Hello world"));