《Java8實戰》-讀書筆記第二章

雷俠發表於2019-02-21

通過行為引數化傳遞程式碼

行為引數化

在《Java8實戰》第二章主要介紹的是通過行為引數化傳遞程式碼,那麼就來了解一下什麼是行為引數化吧。

在軟體工程中,一個從所周知的問題就是,不管你做什麼,使用者的需求總是會變的(PM的需求總是會變的)。比方說,有個應用程式是幫助農民瞭解自己的庫存。這位農民可能想有一個查詢庫存中所有綠色蘋果的功能。但是到了第二天,他突然告訴你:“其實我還想找出所有重量超過150克的蘋果。”,你一想簡單嘛不就是改一下條件而已。於是過了兩天,他又說:“要是我可以篩選即使綠色的蘋果,重量也超過150克的蘋果。”,這樣頻繁的改需求也不太好,面對這樣的情況理想狀態下應該把工作量降到最低。此外,類似的功能實現起來應該還是很簡單,而且利於長期維護。

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

應對不斷變化的需求

想要編寫能應對變化的需求並不容易。讓我們來看一個例子,我們將會逐漸的改進這個例子,以展示一些讓程式碼更靈活的做法。就像農場庫存程式而言,你需要實現一個從列表中篩選綠蘋果的功能。

篩選蘋果

  1. 篩選綠蘋果,可能你選擇最初的解決方案就是這樣:
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;
}
複製程式碼

現在程式碼中就是篩選綠蘋果。但現在農民改主意了,他還想要篩選紅蘋果。按照最簡單的方法就是,把方法複製一下並且改一下條件為篩選紅蘋果的條件。是的,這樣做起來很簡單,要是農民想要篩選多種顏色:青色、深紅、淡紅...這種方法就不太適合了。

  1. 優化程式碼,通過顏色作為引數篩選蘋果:
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克。”你可能早就想到了需要通過重量來篩選蘋果,於是你又把引數穿進來作為條件進行篩選。

  1. 將重量作為引數,進行重蘋果篩選:
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 不要重複自己)的軟體工程原則。或許,你一下就想到了這辦法,將所有的引數都放在一個方法中,這樣就可以簡化很多程式碼了。

  1. 第三次嘗試,對你能想到的每個屬性做篩選:
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"));
複製程式碼

總結

  1. 行為引數化,就是一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力。
  2. 行為引數化可以讓程式碼更好的適應不斷變化的要求,減輕工作量。
  3. 傳遞程式碼,就是將新行為作為引數傳遞給方法。但在Java8之前這實現起來很囉嗦。為介面生命許多隻是用一次的實體類而造成的囉嗦程式碼,在Java8之前採用匿名類來減少。
  4. JavaAPI包含了很多可以用不同行為進行引數化的方法,包括排序、執行緒等。

程式碼示例:

Github:chap2

公眾號

《Java8實戰》-讀書筆記第二章

相關文章