Java函數(shù)式編程之通過行為參數(shù)化傳遞代碼
不斷變化的需求
在軟件工程中,一個眾所周知的問題就是,不管你做什么,用戶的需求肯定會變
比如之前的蘋果的例子
- 找綠色蘋果
- 找紅色蘋果
- 找大于150G的蘋果
- 找大于150G的綠蘋果
- 找大于150G的紅蘋果
- 找大于150G且小于400G的紅蘋果
按照上述經(jīng)常變動,且都需要的,那要寫多少方法?
我們將上述的“需求”看做是一種行為。那在進行處理的時候,只需要傳遞這種“行為”,那么所有的行為方法都不需要寫了,簡直一勞永逸。
我們稱之為行為,而傳遞的行為叫做行為化參數(shù)。行為參數(shù)化就是可以幫助你處理頻繁變更的需求的一種軟件開發(fā)模式。
一言以蔽之,它意味著拿出一個代碼塊,把它準備好卻不去執(zhí)行它。這個代碼塊以后可以被你程序的其他部分調(diào)用,這意味著你可以推遲這塊代碼的執(zhí)行。例如,你可以將代碼塊作為參數(shù)傳遞給另一個方法,稍后再去執(zhí)行它。這樣,這個方法的行為就基于那塊代碼被參數(shù)化了。
假如你要處理一個集合,會寫這樣的一個方法
- 可以對列表中的每個元素做“某件事”
- 可以在列表處理完后做“另一件事”
- 遇到錯誤時可以做“另外一件事”
這就是行為化,如果還不理解,繼續(xù)往下看。
打個比方吧:你的室友知道怎么開車去超市,再開回家。于是你可以告訴他去買一些東西,比如面包、奶酪、葡萄酒什么的。這相當(dāng)于調(diào)用一個goAndBuy方法,把購物單作為參數(shù)。然而,有一天你在上班,你需要他去做一件他從來沒有做過的事情:從郵局取一個包裹。現(xiàn)在你就需要傳遞給他一系列指示了:去郵局,使用單號,和工作人員說明情況,取走包裹。你可以把這些指示用電子郵件發(fā)給他,當(dāng)他收到之后就可以按照指示行事了。你現(xiàn)在做的事情就更高級一些了,相當(dāng)于一個方法:go,它可以接受不同的新行為作為參數(shù),然后去執(zhí)行。
應(yīng)對不斷變化的需求
- 這里會使用一個案例,并且逐步改善
- 篩選綠蘋果
public static List<Apple> filterGreenApples(List<Apple> apples) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { // 篩選 if ("green".equals(apple.getColor())) { arrayList.add(apple); } } return arrayList; }
- 現(xiàn)在做的就是篩選綠蘋果的,現(xiàn)在需求變更,需要篩選紅蘋果,那還要把這個方法拷貝一下,把Green換成Red。很明顯,違反了DRY(Do not Repeat Youself)
- 怎么做呢?把顏色作為參數(shù)
- 因為只是改變顏色,所以把顏色傳遞進去,可以省去一個方法
public static List<Apple> filterApplesByColor(List<Apple> apples, String color) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { if (color != null && color.equals(apple.getColor())) { arrayList.add(apple); } } return arrayList; }
- 上述已經(jīng)完成了操作,此時又有新的需求:要區(qū)分重的蘋果和輕的蘋果,同理
public static List<Apple> filterApplesByWeight(List<Apple> apples, int Weight) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { if (apple.getWeight() > Weight) { arrayList.add(apple); } } return arrayList; }
- 但是還是復(fù)制了代碼,對整個輸出行為有影響的只有那一行判斷條件
- 再次嘗試:對你能所想到的每個屬性進行篩選
public static List<Apple> filterApples(List<Apple> apples, int weight, String color, boolean flag) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { if ((flag && apple.getWeight() > weight) || (!flag && color != null && color.equals(apple.getColor()))) { arrayList.add(apple); } } return arrayList; }
- 這種代碼已經(jīng)臟的不行了,傳遞weight、color、flag,用flag判斷到底哪一個屬性生效,那么可以遇見的是,如果Apple增加了其他的字段:甜度、水分、生產(chǎn)地......
- 那么這個方法已經(jīng)無法維護了,如果需要更加復(fù)雜的查詢,也無法編寫。而且,向一個方法傳遞一個boolean是非常危險的事情,有boolean就意味著有分支。
行為參數(shù)化
經(jīng)過上面的案例,我們需要一種更好的方式來應(yīng)對變化的需求。
現(xiàn)在對上述的需求進行建模之前,先看一下可能的需求案例
- 找綠色蘋果
- 找紅色蘋果
- 找大于150G的蘋果
- 找大于150G的綠蘋果
- 找大于150G的紅蘋果
- 找大于150G且小于400G的紅蘋果
這是上文提到的,我們將所有的過濾條件去掉,那么這些需求案例,就是
找滿足xxx屬性的蘋果
那么建模如下:你考慮的是蘋果,需要根據(jù)Apple的某些屬性,返回一個滿足條件的boolean值。
我們稱之為謂詞,定義接口如下
public interface ApplePredicate { boolean test(Apple apple); }
- 然后可以使用ApplePredicate的多個實現(xiàn)來執(zhí)行不同的行為了
class AppleHeavyWeightPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return apple.getWeight() > 150; } } class AppleRedPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return "red".equals(apple.getColor()); } }
- 這些實現(xiàn),就是不同的篩選條件(filter),也相當(dāng)于不同的策略,算法族就是
ApplePredicate
- 但是怎么利用這種不同的實現(xiàn)呢?需要filterApples方法接受ApplePredicate對象,對Apple做條件測試。
- 這就是行為參數(shù)化:讓方法接受多種行為(或戰(zhàn)略)作為參數(shù),并在內(nèi)部使用,來完成不同的行為。
- 那么現(xiàn)在就來修改之前的代碼
public class Test5 { public static void main(String[] args) { System.out.println(filterApples(AppleClient.getApples(), new AppleHeavyWeightPredicate())); System.out.println(filterApples(AppleClient.getApples(), new AppleRedPredicate())); } public static List<Apple> filterApples(List<Apple> apples, ApplePredicate applePredicate) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { if (applePredicate.test(apple)) { arrayList.add(apple); } } return arrayList; } }
- 現(xiàn)在你把filterApples方法迭代集合的邏輯與你要應(yīng)用到集合中每個元素的行為(這里是一個謂詞)區(qū)分開了。
- 這樣一來,任何需求的變更,只需要增加相應(yīng)的實現(xiàn)類即可。filterApples方法的行為取決于你通過ApplePredicate對象傳遞的代碼。但是會有一個問題:類膨脹。
- 代碼傳遞/行為
- 在上述的實現(xiàn)中,我們發(fā)現(xiàn),對于整個測試,唯一重要的就是
test方法
的實現(xiàn),這個方法決定了需要怎么樣進行過濾。 - 所以在傳遞行為的時候,我們可以直接使用匿名內(nèi)部類來傳遞,如下
public class Test5 { public static void main(String[] args) { System.out.println(filterApples(AppleClient.getApples(), new ApplePredicate() { @Override public boolean test(Apple apple) { return apple.getWeight() > 150; } })); System.out.println(filterApples(AppleClient.getApples(), new ApplePredicate() { @Override public boolean test(Apple apple) { return "red".equals(apple.getColor()); } })); } public static List<Apple> filterApples(List<Apple> apples, ApplePredicate applePredicate) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { if (applePredicate.test(apple)) { arrayList.add(apple); } } return arrayList; } }
- 但是多個很多無用的代碼,此時再將匿名內(nèi)部類更改為Lambda表達式即可
public class Test5 { public static void main(String[] args) { System.out.println(filterApples(AppleClient.getApples(), apple -> apple.getWeight() > 150)); System.out.println(filterApples(AppleClient.getApples(), apple -> "red".equals(apple.getColor()))); } public static List<Apple> filterApples(List<Apple> apples, ApplePredicate applePredicate) { List<Apple> arrayList = new ArrayList<>(); for (Apple apple : apples) { if (applePredicate.test(apple)) { arrayList.add(apple); } } return arrayList; } }
- 如上,你只需要關(guān)注行為,也就是你需要
test
的實現(xiàn)即可。 - 多種行為/一個參數(shù)
- 正如我們先前解釋的那樣,行為參數(shù)化的好處在于你可以把迭代要篩選的集合的邏輯與對集合中每個元素應(yīng)用的行為區(qū)分開來。這樣你可以重復(fù)使用同一個方法,給它不同的行為來達到不同的目的
- 新需求,對蘋果進行遍歷,然后對其進行格式化輸出
- 這個需求需要定義一個新的方法,prettyPrintApple,參照上面的篩選蘋果的案例,整體的執(zhí)行框架如下
public static void prettyPrintApple(List<Apple> apples,???){ for (Apple apple : apples) { String output = ???.???(apple); System.out.println(output); } }
- 其中???的部分就是我們要填充的行為,其比較簡單:輸入一個Apple,然后輸出一個String,那么就來定義這樣的一個接口 FormatApple
public interface AppleFormat { String accept(Apple apple); }
- 那么 prettyPrintApple 就可以實現(xiàn)了,如下
public static void prettyPrintApple(List<Apple> apples,AppleFormat appleFormat){ for (Apple apple : apples) { String output = appleFormat.accept(apple); System.out.println(output); } }
- 這樣就可以表示多種行為了
public class Test6 { public static void main(String[] args) { prettyPrintApple(AppleClient.getApples(), apple -> { return "顏色:" + apple.getColor() + ",重量:" + apple.getWeight(); }); prettyPrintApple(AppleClient.getApples(), apple -> { return "顏色:" + apple.getColor(); }); } public static void prettyPrintApple(List<Apple> apples, AppleFormat appleFormat) { for (Apple apple : apples) { String output = appleFormat.accept(apple); System.out.println(output); } } }
- 到此為止,我們可以將類、匿名類、Lambda進行行為參數(shù)化,替代了之前的案例中的值參數(shù)化。
- 現(xiàn)在我們發(fā)現(xiàn)上述的
ApplePredicate
只能處理Apple,且filterApples
也只能處理Apple,所以我們將這部分使用泛型進行抽象化,如下
public interface Predicate<T> { boolean test(T t); }
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) { List<T> result = new ArrayList<>(); for (T e : list) { if (predicate.test(e)){ result.add(e); } } return result; }
- 通過這樣的處理,所有類型的數(shù)據(jù),都可以這樣進行篩選出結(jié)果。
- 其他的行為參數(shù)化案例
- 對集合進行排序,根據(jù)蘋果顏色排序或者根據(jù)大小排序
- 在Java 8中,List自帶了一個sort方法(你也可以使用Collections.sort)來進行排序
- sort的行為可以用java.util.Comparator對象來參數(shù)化,它的接口如下
public interface Comparator<T>{ int compare(T o1, T o2); }
- 因此,你可以隨時創(chuàng)建Comparator的實現(xiàn),用sort方法表現(xiàn)出不同的行為。比如,你可以使用匿名類,按照重量升序?qū)齑媾判?/li>
public class Test8 { public static void main(String[] args) { AppleClient.getApples().sort(new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight() - o2.getWeight(); } }); } }
- 或者是Lambda表達式
public class Test8 { public static void main(String[] args) { AppleClient.getApples().sort((o1, o2) -> o1.getWeight() - o2.getWeight()); } }
- 如果對匿名類轉(zhuǎn)Lambda表達式不熟悉,我們會在下面進行講解。
小結(jié)
- 行為參數(shù)化,就是一個方法接受多個不同的行為作為參數(shù),并在內(nèi)部使用它們,完成不同行為的能力。
- 行為參數(shù)化可讓代碼更好地適應(yīng)不斷變化的要求,減輕未來的工作量。
- 傳遞代碼,就是將新行為作為參數(shù)傳遞給方法。但在Java 8之前這實現(xiàn)起來很啰嗦。為接口聲明許多只用一次的實體類而造成的啰嗦代碼,在Java 8之前可以用匿名類來減少。
- Java API包含很多可以用不同行為進行參數(shù)化的方法,包括排序、線程和GUI處理。
以上就是Java函數(shù)式編程之通過行為參數(shù)化傳遞代碼的詳細內(nèi)容,更多關(guān)于Java行為參數(shù)化傳遞代碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java使用JSQLParser解析和操作SQL的技術(shù)指南
在開發(fā)過程中,解析和操作?SQL?是一個常見的需求,JSQLParser?是一個強大的開源?Java?庫,用于解析?SQL?并提供語法樹操作功能,本文將詳細介紹如何使用?JSQLParser,并提供常見使用場景的代碼示例,需要的朋友可以參考下2025-04-04Spring rest接口中的LocalDateTime日期類型轉(zhuǎn)時間戳
這篇文章主要介紹了Spring rest接口中的LocalDateTime日期類型轉(zhuǎn)時間戳的方法,Java程序中一般將日期類型定義為LocalDateTime,數(shù)據(jù)庫中保存的時間是0時區(qū)的時間2023-03-03關(guān)于MyBatis中SqlSessionFactory和SqlSession簡解
這篇文章主要介紹了MyBatis中SqlSessionFactory和SqlSession簡解,具有很好的參考價值,希望大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12java中 IO 常用IO操作類繼承結(jié)構(gòu)分析
本篇文章小編為大家介紹,java中 IO 常用IO操作類繼承結(jié)構(gòu)分析。需要的朋友參考下2013-04-04SpringBoot返回統(tǒng)一的JSON標準格式實現(xiàn)步驟
這篇文章主要介紹了SpringBoot返回統(tǒng)一的JSON標準格式,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08Java并發(fā)編程中的CyclicBarrier線程屏障詳解
這篇文章主要介紹了Java并發(fā)編程中的CyclicBarrier線程屏障詳解,2023-12-12Hikari連接池使用SpringBoot配置JMX監(jiān)控實現(xiàn)
Hikari是Spring Boot默認的數(shù)據(jù)庫連接池。區(qū)別于C3P0直接通過連接池對象獲取各項狀態(tài)指標,Hikari需要通過JMX來獲取。本文就詳細的來介紹一下,感興趣的可以了解一下2021-07-07springboot批量接收對象參數(shù),接收List方式
在Spring Boot項目中,批量接收對象參數(shù)可以通過自定義對象和使用`@RequestBody`注解來實現(xiàn),首先,定義一個包含列表的自定義對象,然后在Controller中使用該對象接收前端傳遞的JSON數(shù)組,通過Postman模擬請求,可以成功批量接收并處理對象參數(shù)2025-02-02