通過(guò)行為參數(shù)傳遞代碼

應(yīng)對(duì)不斷變化的需求

目標(biāo)

在軟件工程中一個(gè)眾所周知的問(wèn)題就是,不管你做什么,用戶的需求肯定會(huì)變。比如一位農(nóng)民第一天可能有一個(gè)想要查找?guī)齑嬷兴芯G色蘋果的功能,但第二天可能又想要找出重量大于 150 克的水果,可能過(guò)幾天又會(huì)變成既要綠色蘋果重量又要大于 150 克。我們?cè)撊绾螒?yīng)對(duì)不斷變化的需求?我們最理想的狀態(tài)為:工作量降到最少,新添加的功能實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單而且易于長(zhǎng)期維護(hù)

行為參數(shù)化是什么

行為參數(shù)化就是可以幫助你處理頻繁變更的需求的一種軟件開(kāi)發(fā)模式。一言以蔽之,它意味著拿出一個(gè)代碼塊,把他準(zhǔn)備好卻不去執(zhí)行它。這個(gè)代碼塊以后可以被你程序的其他部分調(diào)用,這意味著你可以推遲這塊代碼的執(zhí)行。例如,你可以將代碼塊作為參數(shù)傳遞給另一個(gè)方法,稍后再去執(zhí)行它。這樣,這個(gè)方法的行為就基于那塊代碼被參數(shù)化了。

下面我們結(jié)合上面提到的農(nóng)民的例子來(lái)應(yīng)對(duì)不斷變化的需求。

篩選綠蘋果

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

篩選出綠色蘋果很簡(jiǎn)單吧,但是農(nóng)民如果要篩選紅色蘋果呢?最簡(jiǎn)單的解決方法就是復(fù)制這個(gè)方法,把名字改成 filterRedApples,然后改一下 if 條件來(lái)匹配紅蘋果。然而如果顏色更多呢?我們不可能寫(xiě)一大溜的方法來(lái)匹配不同的顏色吧!一個(gè)良好的原則是在編寫(xiě)類似的代碼后嘗試將其抽象化

把顏色作為參數(shù)

一種做法是給方法加一個(gè)參數(shù),把顏色變成參數(shù),這樣就能靈活地適應(yīng)變化了:

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

只要像下面這樣調(diào)用就可以了:

    List<Apple> greenApples=filterApplesByColor(inventory,"green");
    List<Apple> redApples=filterApplesByColor(inventory,"red");

太簡(jiǎn)單了吧?那如果農(nóng)民想要區(qū)分輕的蘋果和重的蘋果呢?

對(duì)你能想到的每個(gè)屬性做篩選

我們寫(xiě)一個(gè)顏色和重量結(jié)合的方法,再加上一個(gè) flag 來(lái)區(qū)分想要篩選哪個(gè)屬性。

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

調(diào)用:

    List<Apple> greenApples = filterApples(inventory, "green",150,true);
    List<Apple> redApples = filterApples(inventory, "red",120,false);

上面的方法好像依然很輕松的解決了農(nóng)民朋友的問(wèn)題,但是客戶端代碼看起來(lái)糟透了,true 和 false 又是什么意思?此外解決方案還是不能很好的應(yīng)對(duì)變化的需求。如果這位農(nóng)民要求你對(duì)蘋果的不同屬性做篩選,比如大小、形狀、產(chǎn)地等,又怎么辦?而且農(nóng)民要求你組合屬性,做更復(fù)雜的查詢,又該怎么辦?你會(huì)有好多重復(fù)的 filter 方法,或一個(gè)巨大的非常復(fù)雜的方法。我們下一節(jié)就開(kāi)始利用行為參數(shù)化來(lái)實(shí)現(xiàn)更加靈活的方法。

行為參數(shù)化

首先我們后退一步來(lái)進(jìn)行更高層次的抽象:我們考慮的是蘋果,需要根據(jù) Apple 的一些屬性(比如它是綠色的嗎?重量超過(guò) 150 克嗎?)來(lái)返回一個(gè) boolean 值。我們把它稱為謂詞(即一個(gè)返回 boolean 值的函數(shù))。讓我們定義一個(gè)接口來(lái)對(duì)選擇標(biāo)準(zhǔn)建模:

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

現(xiàn)在我們可以使用 ApplePredicate 的多個(gè)實(shí)現(xiàn)代表不同的選擇標(biāo)準(zhǔn)了:

篩選重量大于 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());
    }
}

我們這樣做已經(jīng)非常類似“策略設(shè)計(jì)模式”了,在這里算法族就是 ApplePredicate,不同的策略就是 AppleHeavyWeightPredicate 或者 AppleGreenColorPredicate。

在我們的例子中根據(jù)抽象條件篩選:

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

現(xiàn)在我們的代碼已經(jīng)靈活多了,讀起來(lái)用起來(lái)都更容易!如果你要篩選紅色并且重量大于 200 克的蘋果只需要再寫(xiě)一個(gè)策略:

public class AppleRedAndHeavyPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor())&&apple.getWeight()>200;
    }
}

使用匿名類加上 Lambda 表達(dá)式:

使用匿名內(nèi)部類:

我們上面所做的,依然有很多不足,我們需要聲明很多只要實(shí)例化一次的類,費(fèi)那么大的勁真的沒(méi)必要,我們可以做的更好嗎?Java 中有匿名內(nèi)部類,我們利用它進(jìn)一步改善代碼:

    List<Apple> greenApples = filterApples(inventory, new ApplePredicate() {
        @Override
        public boolean test(Apple apple) {
            return "green".equals(apple.getColor());
        }
    });

使用 Lambda表達(dá)式

匿名內(nèi)部類還是不夠好,它比較笨重,占用了很多的空間。而且內(nèi)部類一般比較難讀。在 Java8 中新添加了 Lambda 表達(dá)式,我們?nèi)绻昧怂恍枰痪湓捑徒鉀Q了問(wèn)題:

    List<Apple> greenApples = filterApples(inventory, (Apple apple)->"green".equals(apple.getColor()));

將 List 類型抽象化

通往抽象的路上,我們還可以更進(jìn)一步。目前我們還只適用于 Apple,我們還可以將 List 類型抽象化,從而超越眼前所要處理的問(wèn)題:

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

現(xiàn)在我們可以把filter方法用在香蕉、桔子、 Integer 或者是 String 上了,比如說(shuō)我們要找出一個(gè)數(shù)字集合中的所有偶數(shù),同樣只需要一句話:

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

Lambda 表達(dá)式鞏固練習(xí)

用 Comparator 升序排序

    //對(duì)蘋果重量進(jìn)行排序
    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 表達(dá)式
    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 的簡(jiǎn)單使用

提前準(zhǔn)備:

defaultConfig {
    ...
    jackOptions {
        enabled true
    }
}

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

點(diǎn)擊事件調(diào)用:

    findViewById(R.id.btn).setOnClickListener((v) -> Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show());

參考

本文純屬《Java8 實(shí)戰(zhàn)》學(xué)習(xí)筆記

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容