通過行為參數化傳遞代碼
行為參數化
在《Java8實戰》第二章主要介紹的是
通過行為參數化傳遞代碼
,那麼就來了解一下什麼是
行為參數化
吧。
在軟體工程中,一個從所周知的問題就是,不管你做什麼,使用者的需求總是會變的(PM的需求總是會變的)。比方說,有個應用程式是幫助農民了解自己的庫存。這位農民可能想有一個查找庫存中所有綠色蘋果的功能。但是到了第二天,他突然告訴你:“其實我還想找出所有重量超過150克的蘋果。”,你一想簡單嘛不就是改一下條件而已。于是過了兩天,他又說:“要是我可以篩選即使綠色的蘋果,重量也超過150克的蘋果。”,這樣頻繁的改需求也不太好,面對這樣的情況理想狀态下應該把工作量降到最低。此外,類似的功能實作起來應該還是很簡單,而且利于長期維護。
行為參數化就是要幫助你處理頻繁更變的需求的一種軟體開發模式。一言以蔽之,它意味着拿出一個代碼塊,把它準備好卻不去執行它。這個代碼塊以後可以被你程式的其他部分調用,這意味着你可以推遲這塊代碼的執行。例如,你可以将代碼塊作為參數傳遞給另外一個方法,稍後再去執行它。這樣,這個方法的行為就基于那塊代碼被參數化了。
應對不斷變化的需求
想要編寫能應對變化的需求并不容易。讓我們來看一個例子,我們将會逐漸的改進這個例子,以展示一些讓代碼更靈活的做法。就像農場庫存程式而言,你需要實作一個從清單中篩選綠蘋果的功能。
篩選蘋果
- 篩選綠蘋果,可能你選擇最初的解決方案就是這樣:
private static List<Apple> filterGreenApples(List<Apple> apples) {
List<Apple> appleList = new ArrayList<Apple>();
for (Apple apple : apples) {
if ("green".equals(apple.getColor())) {
appleList.add(apple);
}
}
return appleList;
}
現在代碼中就是篩選綠蘋果。但現在農民改主意了,他還想要篩選紅蘋果。按照最簡單的方法就是,把方法複制一下并且改一下條件為篩選紅蘋果的條件。是的,這樣做起來很簡單,要是農民想要篩選多種顔色:青色、深紅、淡紅...這種方法就不太适合了。
- 優化代碼,通過顔色作為參數篩選蘋果:
private static List<Apple> filterApplesByColor(List<Apple> apples, String color) {
List<Apple> appleList = new ArrayList<Apple>();
for (Apple apple : apples) {
if (color.equals(apple.getColor())) {
appleList.add(apple);
}
}
return appleList;
}
很簡單對吧。現在,農民又有想法:“能篩選出輕蘋果和重蘋果就好啦!一般重蘋果的重量是150克。”你可能早就想到了需要通過重量來篩選蘋果,于是你又把參數穿進來作為條件進行篩選。
- 将重量作為參數,進行重蘋果篩選:
private static List<Apple> filterApplesByWeight(List<Apple> apples, int weight) {
List<Apple> appleList = new ArrayList<Apple>();
for (Apple apple : apples) {
if (apple.getWeight() > weight) {
appleList.add(apple);
}
}
return appleList;
}
是的,解決方法很簡單,但是你複制了大部分的代碼來實作周遊庫存,并對每個蘋果應用篩選條件。這樣破壞了DRY(Don't Repeat Yourself 不要重複自己)的軟體工程原則。或許,你一下就想到了這辦法,将所有的參數都放在一個方法中,這樣就可以簡化很多代碼了。
- 第三次嘗試,對你能想到的每個屬性做篩選:
private static List<Apple> filterApples(List<Apple> apples, String color, int weight, boolean flag) {
List<Apple> appleList = new ArrayList<Apple>();
for (Apple apple : apples) {
boolean result = (flag && apple.getWeight() > weight) || (!flag && color.equals(apple.getColor()));
if (result) {
appleList.add(apple);
}
}
return appleList;
}
代碼看起來很簡單,但是感覺卻是不太好。如果不把注釋寫清楚,别人閱讀你代碼時根本就不知道flag是幹嘛用的。要是,農民突然又有個想法,需用通過大小、形狀、産地等條件來進行篩選怎麼辦?是以,我們需要利用行為參數化來解決這個問題,提高代碼的靈活性。
目前,你需要一種比添加很多參數更好的方法來應對變化的需求。讓我們退一步來看看更高層次的抽象。一種可能解決方案是對你的懸着标準模組化:你考慮的是蘋果,需要根據Apple的某些屬性(比如它是綠色的嗎?重量超過150克嗎?)來傳回一個boolean值。是的,你可能已經想到了第一章中介紹到了的謂詞。
根據謂詞進行篩選
首先,我們應該定義一個接口來對選擇标準模組化:
public interface ApplePredicate {
/**
* 根據給定的參數計算此謂詞。
*
* @param apple
* @return
*/
boolean test(Apple apple);
}
可以用ApplePredicate的實作類來代表不同的選擇标準:
隻篩選綠蘋果
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
隻篩選重蘋果
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
你可以把這些标準看作filter的不同行為。這就像政策設計模式一樣,它讓你定義一組方法,把它們封裝起來,然後在運作時選擇一個方法。這裡,方法就是ApplePredicate,不同的政策就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
你可以将filterApples方法接受一個ApplePredicate對象,對Apple做條件測試。這樣就是行為參數化:讓方法接受多種行為作為參數,并在内部使用,來完成不同的行為。
根據抽象條件篩選
private static List<Apple> filterApples(List<Apple> apples, ApplePredicate<Apple> applePredicate) {
List<Apple> appleList = new ArrayList<>();
for (Apple apple : apples) {
if (applePredicate.test(apple)) {
appleList.add(apple);
}
}
return appleList;
}
代碼的傳遞/行為
酷,這段代碼看起來很多了,讀起來、用起來也更容易!現在你可以建立不同的ApplePredicate對象,并将它們傳遞給filterApples方法。這樣就可以根據不同的條件來建立一個類并且實作ApplePredicate就可以了。
現在,農民要求需要篩選紅蘋果。那麼,我們就可以根據條件建立一個類并且實作ApplePredicate:
public class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}
List<Apple> filterApples2 = filterApples(apples, new AppleRedAndHeavyPredicate());
System.out.println("通過謂詞篩選紅蘋果并且是重蘋果:" + filterApples2);
酷,現在filterApples方法的行為已經取決于通過ApplePredicate對象來實作了。這就是行為參數化了!
但是,你有沒有發現,我們每次新增一個條件就需要新增一個類。這樣做有點太過于麻煩,或許我們可以通過Lambda,将表達式傳遞給filterApples方法,這樣就無需定義多個ApplePredicate類,進而去掉不必要的代碼,并減輕工作量。
多種行為,一個參數
行為參數化的好處在與你可以把疊代要篩選的集合的邏輯與對集合中每個元素應用的行為區分開來。這樣你就可以重複使用同一個方法,給它不同的行為來達到不同的目的。
為了能夠對參數化行為運用自如,并且簡化代碼,我們來嘗試将參數通過Lambda的方式傳遞給filterApples。
通過Lambda的方式來篩選紅蘋果:
List<Apple> filterApples3 = filterApples(apples, apple -> "red".equals(apple.getColor()));
通過Lambda的方式來篩選紅蘋果并且是重蘋果:
List<Apple> filterApples4 = filterApples(apples, apple -> "red".equals(apple.getColor()) && apple.getWeight() > 150);
是的,使用的已經還是原來的條件,并且不再需要根據不同的條件再去實作一個ApplePredicate類了,這樣極大的簡化了代碼。但是,農民又有一個需求了:“現在,不隻是需要對蘋果進行篩選了,還需要對香蕉、橘子、草莓進行篩選了。”
但是,我們目前的代碼隻能夠對蘋果進行篩選而已,為了解決這個問題,我們可以将類型定義為泛型,這樣就不隻是隻能對蘋果進行篩選了。
使用謂詞
其實,我們可以不需要去定義謂詞,因為在Java中就一個了Predicate了,我們可以使用它并且實作我們的功能。
定義一個泛型的filter方法:
private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (predicate.test(t)) {
result.add(t);
}
}
return result;
}
這個方法是一個通用的篩選方法,不隻是可以用于篩選蘋果。
篩選重蘋果:
List<Apple> heavyApples = filter(apples, (Apple apple) -> apple.getWeight() > 150);
篩選能被2整除的數:
List<Integer> numbers = Arrays.asList(10, 11, 8, 5, 1, 2, 29, 18);
List<Integer> integerList = filter(numbers, number -> number % 2 == 0);
是不是很酷?現在的代碼簡潔性和靈活性都很高,在Java8之前這些都是不可能做到的!
現在,你已經感覺到了行為參數化是一個很有用的模式,它能夠輕松地适應不斷變化的需求。在Java中很多方法都可以用不同的行為來參數化,比如使用Comparator排序,用Runnable執行一個代碼塊等等。
使用Comparator來排序:
apples.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
或者這樣:
apples.sort(Comparator.comparing(Apple::getWeight));
使用Runnable執行某個代碼塊:
Thread t = new Thread(() -> System.out.println("HelloWorld"));
總結
- 行為參數化,就是一個方法接受多個不同的行為作為參數,并在内部使用它們,完成不同行為的能力。
- 行為參數化可以讓代碼更好的适應不斷變化的要求,減輕工作量。
- 傳遞代碼,就是将新行為作為參數傳遞給方法。但在Java8之前這實作起來很啰嗦。為接口生命許多隻是用一次的實體類而造成的啰嗦代碼,在Java8之前采用匿名類來減少。
- JavaAPI包含了很多可以用不同行為進行參數化的方法,包括排序、線程等。
代碼示例:
Github:
chap2公衆号
