在軟件工程中,一個眾所周知的問題就是,不管做什么,用戶的需求肯定會變。
如何應對這樣不斷變化的需求?理想的狀態下,應該把的工作量降到最少。此外,類似的新功能實現起來還應該很簡單,而且易於長期維護。
行為參數化就是可以幫助處理頻繁變更的需求的一種軟件開發模式。一言以蔽之,它意味着拿出一個代碼塊,把它准備好卻不去執行它。這個代碼塊以后可以被程序的其他部分調用,這意味着可以推遲這塊代碼的執行。
以篩選蘋果為例,逐步改進代碼,來展示一些讓代碼更靈活的最佳做法。
需求:篩選綠色蘋果
1.第一次嘗試:為了實現篩選綠蘋果,for循環篩選綠蘋果
List<Apple> inventory = Arrays.asList(new Apple(80, "green"), new Apple(155, "green"), new Apple(120, "red")); public class Apple { private int weight; private String color;
// get setter ...
}
List<Apple> apples = filterGreenApples(inventory); 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; }
需求變化:篩選其他顏色的蘋果
2.第二次嘗試:把顏色作為參數,篩選對應顏色
List<Apple> apples = filterApplesByColor(inventory,"red"); 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; }
需求變化:刷選顏色加重量
3.第三次嘗試:對能想到的每個屬性做篩選
public static List<Apple> filterApples(List<Apple> inventory, String color,int weight) { //...
這個解決方案還是不能很好地應對變化的需求。
4.第四次嘗試:根據抽象條件篩選
List<Apple> apples = filterApples(inventory, new AppleColorPredicate()); 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; } interface ApplePredicate { public boolean test(Apple a); } 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()); } }
附:剛做的這些和“策略設計模式”相關,它讓定義一族算法,把它們封裝起來(稱為“策略”),然后在運行時選擇一個算法。在這里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。 但是,該怎么利用ApplePredicate的不同實現呢?需要filterApples方法接受ApplePredicate對象,對Apple做條件測試。這就是行為參數化:讓方法接受多種行為(或戰略)作為參數,並在內部使用,來完成不同的行為。 要在我們的例子中實現這一點,要給filterApples方法添加一個參數,讓它接受ApplePredicate對象。這在軟件工程上有很大好處:現在把filterApples方法迭代集合的邏輯與要應用到集合中每個元素的行為區分開了。
請注意,在這個例子中,唯一重要的代碼是test方法的實現;正是它定義了filterApples方法的新行為。由於該filterApples方法只能接受對象,所以必須把代碼包裹在ApplePredicate對象里。的做法就類似於在內聯“傳遞代碼”,因為是通過一個實現了test方法的對象來傳遞布爾表達式的。
這種行為參數化的好處在於可以把迭代要篩選的集合的邏輯與對集合中每個元素應用的行為區分開來。這樣可以重復使用同一個方法,給它不同的行為來達到不同的目的
5. 第五次嘗試:使用匿名類
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { // 直接內聯參數化filterapples方法的行為 public boolean test(Apple apple) { return "red".equals(apple.getColor()); } });
附:匿名類和熟悉的Java局部類(塊中定義的類)差不多,但匿名類沒有名字。它允許同時聲明並實例化一個類。換句話說,它允許隨用隨建。
6.第六次嘗試:使用Lambda表達式
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
7.第七次嘗試:將List類型抽象化
在通往抽象的路上,我們還可以更進一步。目前,filterApples方法還只適用於Apple。還可以將List類型抽象化,從而超越眼前要處理的問題:
public interface Predicate<T> { boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p) { //引入類型參數T List<T> result = new ArrayList<>(); for (T e : list) { if (p.test(e)) { result.add(e); } } return result; }
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor())); System.out.println(redApples); List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0); System.out.println(evenNumbers);
參考:java8實戰第二章