應對不斷變化的需求
目標
在軟件工程中一個眾所周知的問題就是,不管你做什么,用戶的需求肯定會變。比如一位農民第一天可能有一個想要查找庫存中所有綠色蘋果的功能,但第二天可能又想要找出重量大于 150 克的水果,可能過幾天又會變成既要綠色蘋果重量又要大于 150 克。我們該如何應對不斷變化的需求?我們最理想的狀態為:工作量降到最少,新添加的功能實現起來非常簡單而且易于長期維護。
行為參數化是什么
行為參數化就是可以幫助你處理頻繁變更的需求的一種軟件開發模式。一言以蔽之,它意味著拿出一個代碼塊,把他準備好卻不去執行它。這個代碼塊以后可以被你程序的其他部分調用,這意味著你可以推遲這塊代碼的執行。例如,你可以將代碼塊作為參數傳遞給另一個方法,稍后再去執行它。這樣,這個方法的行為就基于那塊代碼被參數化了。
下面我們結合上面提到的農民的例子來應對不斷變化的需求。
篩選綠蘋果
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;
}
篩選出綠色蘋果很簡單吧,但是農民如果要篩選紅色蘋果呢?最簡單的解決方法就是復制這個方法,把名字改成 filterRedApples,然后改一下 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;
}
只要像下面這樣調用就可以了:
List<Apple> greenApples=filterApplesByColor(inventory,"green");
List<Apple> redApples=filterApplesByColor(inventory,"red");
太簡單了吧?那如果農民想要區分輕的蘋果和重的蘋果呢?
對你能想到的每個屬性做篩選
我們寫一個顏色和重量結合的方法,再加上一個 flag 來區分想要篩選哪個屬性。
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",150,true);
List<Apple> redApples = filterApples(inventory, "red",120,false);
上面的方法好像依然很輕松的解決了農民朋友的問題,但是客戶端代碼看起來糟透了,true 和 false 又是什么意思?此外解決方案還是不能很好的應對變化的需求。如果這位農民要求你對蘋果的不同屬性做篩選,比如大小、形狀、產地等,又怎么辦?而且農民要求你組合屬性,做更復雜的查詢,又該怎么辦?你會有好多重復的 filter 方法,或一個巨大的非常復雜的方法。我們下一節就開始利用行為參數化來實現更加靈活的方法。
行為參數化
首先我們后退一步來進行更高層次的抽象:我們考慮的是蘋果,需要根據 Apple 的一些屬性(比如它是綠色的嗎?重量超過 150 克嗎?)來返回一個 boolean 值。我們把它稱為謂詞(即一個返回 boolean 值的函數)。讓我們定義一個接口來對選擇標準建模:
public interface ApplePredicate {
boolean test (Apple apple);
}
現在我們可以使用 ApplePredicate 的多個實現代表不同的選擇標準了:
篩選重量大于 150 的蘋果:
public class AppleHeavyWeightPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return apple.getWeight()>150;
}
}
篩選綠色蘋果:
public class AppleGreenColorPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
我們這樣做已經非常類似“策略設計模式”了,在這里算法族就是 ApplePredicate,不同的策略就是 AppleHeavyWeightPredicate 或者 AppleGreenColorPredicate。
在我們的例子中根據抽象條件篩選:
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;
}
現在我們的代碼已經靈活多了,讀起來用起來都更容易!如果你要篩選紅色并且重量大于 200 克的蘋果只需要再寫一個策略:
public class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor())&&apple.getWeight()>200;
}
}
使用匿名類加上 Lambda 表達式:
使用匿名內部類:
我們上面所做的,依然有很多不足,我們需要聲明很多只要實例化一次的類,費那么大的勁真的沒必要,我們可以做的更好嗎?Java 中有匿名內部類,我們利用它進一步改善代碼:
List<Apple> greenApples = filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
});
使用 Lambda表達式
匿名內部類還是不夠好,它比較笨重,占用了很多的空間。而且內部類一般比較難讀。在 Java8 中新添加了 Lambda 表達式,我們如果利用了它只需要一句話就解決了問題:
List<Apple> greenApples = filterApples(inventory, (Apple apple)->"green".equals(apple.getColor()));
將 List 類型抽象化
通往抽象的路上,我們還可以更進一步。目前我們還只適用于 Apple,我們還可以將 List 類型抽象化,從而超越眼前所要處理的問題:
public interface Predicate<T>{
boolean test(T t);
}
修改filter方法:
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;
}
現在我們可以把filter方法用在香蕉、桔子、 Integer 或者是 String 上了,比如說我們要找出一個數字集合中的所有偶數,同樣只需要一句話:
List<Integer> evenNumbers=filter(numbers,(i)->i%2==0);
Lambda 表達式鞏固練習
用 Comparator 升序排序
//對蘋果重量進行排序
List<Apple> inventory = new ArrayList<>();
inventory.add(new Apple("green", 155));
inventory.add(new Apple("green", 177));
inventory.add(new Apple("green", 244));
inventory.add(new Apple("red", 123));
//原始方法
//Collections.sort(inventory, new Comparator<Apple>() {
// @Override
// public int compare(Apple a1, Apple a2) {
// return a1.getWeight() > a2.getWeight() ? -1 : a1.getWeight() < a2.getWeight() ? 1 : 0;
// }
//});
//使用 Lambda 表達式
Collections.sort(inventory, (a1, a2) -> a1.getWeight() > a2.getWeight() ? 1 : a1.getWeight() < a2.getWeight() ? -1 : 0);
for (Apple apple : inventory) {
System.out.println(apple.getWeight());
}
Thread 的使用
new Thread(()->System.out.println("Hello world")).start();
Android 中 Lambda 的簡單使用
提前準備:
defaultConfig {
...
jackOptions {
enabled true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
點擊事件調用:
findViewById(R.id.btn).setOnClickListener((v) -> Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show());
參考
本文純屬《Java8 實戰》學習筆記