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