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