1. 程式人生 > >Java 8 學習筆記2——通過行為引數化傳遞程式碼

Java 8 學習筆記2——通過行為引數化傳遞程式碼

行為引數化就是可以幫助你處理頻繁變更的需求的一種軟體開發模式。一言以蔽之,它意味著拿出一個程式碼塊,把它準備好卻不去執行它。這個程式碼塊以後可以被你程式的其他部分呼叫,這意味著你可以推遲這塊程式碼的執行。例如,你可以將程式碼塊作為引數傳遞給另一個方法,稍後再去執行它。這樣,這個方法的行為就基於那塊程式碼被引數化了。例如,如果你要處理一個集合,可能會寫一個方法:

  • 可以對列表中的每個元素做“某件事”
  • 可以在列表處理完後做“另一件事”
  • 遇到錯誤時可以做“另外一件事”

行為引數化說的就是這個。打個比方吧:你的室友知道怎麼開車去超市,再開回家。於是你可以告訴他去買一些東西,比如麵包、乳酪、葡萄酒什麼的。這相當於呼叫一個goAndBuy

方法,把購物單作為引數。然而,有一天你在上班,你需要他去做一件他從來沒有做過的事情:從郵局取一個包裹。現在你就需要傳遞給他一系列指示了:去郵局,使用單號,和工作人員說明情況,取走包裹。你可以把這些指示用電子郵件發給他,當他收到之後就可以按照指示行事了。你現在做的事情就更高階一些了,相當於一個方法:go,它可以接受不同的新行為作為引數,然後去執行。

行為引數化,就是一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力。

行為引數化可以讓程式碼更好地適應不斷變化的要求,減輕未來的工作量。

傳遞程式碼,就是將新行為作為引數傳遞給方法。

Java API包含很多可以用不同行為進行引數化的方法,包括排序、執行緒和GUI

處理。

行為引數化

以篩選蘋果為例,對選擇標準建模:需要根據Apple的某些屬性(比如它是綠色的嗎?重量超過150克嗎?)來返回一個boolean值。我們把它稱為謂詞(即一個返回boolean值的函式)。下面定義一個介面來對選擇標準建模

public interface ApplePredicate {
	boolean test(Apple apple);
}

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

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());
	}

}

在這裡插入圖片描述
可以把這些標準看作filter方法的不同行為。剛剛做的這些和“策略設計模式”相關,它讓你定義一族演算法,把它們封裝起來(稱為“策略”),然後在執行時選擇一個演算法。在這裡,演算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicateAppleGreenColorPredicate

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

要在前面的例子中實現這一點,要給filterApples方法新增一個引數,讓它接受ApplePredicate物件。這在軟體工程上有很大好處:現在把filterApples方法迭代集合的邏輯與你要應用到集合中每個元素的行為(這裡是一個謂詞)區分開了。

下面根據抽象條件篩選。

利用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;
}

現在可以建立不同的ApplePredicate物件,並將它們傳遞給filterApples方法。免費的靈活性!比如,如果現在讓你找出所有重量超過150克的紅蘋果,你只需要建立一個類來實現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());

於是,filterApples方法的行為將取決於你通過ApplePredicate物件傳遞的程式碼換句話說,你把filterApples方法的行為引數化了。

需要注意的是,在上面的例子中,唯一重要的程式碼是test方法的實現,如下圖所示;正是它定義了filterApples方法的新行為。但令人遺憾的是,由於該filterApples方法只能接受物件,所以你必須把程式碼包裹在ApplePredicate物件裡。你的做法就類似於在內聯“傳遞程式碼”,因為你是通過一個實現了test方法的物件來傳遞布林表示式的。
在這裡插入圖片描述
行為引數化的好處在於你可以把迭代要篩選的集合的邏輯與對集合中每個元素的應用的行為區分開來。這樣你可以重複使用同一個方法,給它不同的行為來達到不同的目的,如下圖所示。
在這裡插入圖片描述

行為引數化步驟整理

下面編寫一個prettyPrintApple方法,它接受一個AppleList,並可以對它引數化,以多種方式根據蘋果生成一個String輸出。例如,你可以告訴prettyPrintApple方法,只打印每個蘋果的重量。此外,你可以讓prettyPrintApple方法分別列印每個蘋果,然後說明它是重的還是輕的。

  1. 首先,你需要一直表示接受Apple並返回一個格式String值的方法。
public interface AppleFormatter {

	String accept(Apple a);
}
  1. 現在你就可以通過實現AppleFormatter方法,來表示多種格式行為了:
public class AppleFancyFormatter implements AppleFormatter {

	public String accept(Apple a) {
		String characteristic=a.getWeight() > 150 ? "heavy" : "light";
		return "A "+characteristic+" "+a.getColor()+" apple";
	}
}
public class AppleSimpleFormatter implements AppleFormatter {

	public String accept(Apple a) {
		return "An apple of "+a.getWeight()+"g";
	}
}
  1. 最後,你需要告訴prettyPrintApple方法接受AppleFormatter物件,並在內部使用它們。你可以給prettyPrintApple加上一個引數:
public static void prettyPrintApple(List<Apple> inventory,AppleFormatter formatter){
	for(Apple apple:inventory){
		String output=formatter.accept(apple);
		System.out.println(output);
	}
}
  1. 現在就可以給prettyPrintApple方法傳遞多種行為了。為此,你首先要例項化AppleFormatter的實現,然後把它們作為引數傳給prettyPrintApple
prettyPrintApple(inventory,new AppleFancyFormatter());

或者

prettyPrintApple(inventory,new AppleSimpleFormatter());

這樣就可以把行為抽象出來,讓你的程式碼適應需求的變化,但這個過程很囉嗦,因為你需要宣告很多隻要例項化一次的類。

使用Lambda表示式

接下來,通過使用Lambda,你可以直接把表示式"red".equals(apple.getColor()) && apple.getWeight() > 150傳遞給filterApples方法,而無需定義多個ApplePredicate類,從而去掉不必要的程式碼。

List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()) && apple.getWeight() > 150);

這程式碼看上去比先前乾淨了很多,看起來也更像問題陳述本身了。
在這裡插入圖片描述
在通往抽象的路上,我們還可以更進一步。目前,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;
}

現在你可以把filter方法用在香蕉、桔子、Integer或是String的列表上了。這裡有一個使用Lambda表示式的例子:

List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));

List<Integer> evenNumbers=filter(numbers, (Integer i) -> i % 2==0);