利用 Function 接口告別冗余(屎山)代碼

前言

在Java開發(fā)的征途中,我們時(shí)常與重復(fù)代碼不期而遇。這些重復(fù)代碼不僅讓項(xiàng)目顯得笨重,更增加了維護(hù)成本。幸運(yùn)的是,Java 8帶來了函數(shù)式編程的春風(fēng),以Function接口為代表的一系列新特性,為我們提供了破除這一難題的利劍。本文將以一個(gè)實(shí)際應(yīng)用場景為例,即使用Java 8的函數(shù)式編程特性來重構(gòu)數(shù)據(jù)有效性斷言邏輯,展示如何通過SFunction(基于Java 8的Lambda表達(dá)式封裝)減少代碼重復(fù),從而提升代碼的優(yōu)雅性和可維護(hù)性。

背景故事:數(shù)據(jù)校驗(yàn)的煩惱

想象一下,在一個(gè)復(fù)雜的業(yè)務(wù)系統(tǒng)中,我們可能需要頻繁地驗(yàn)證數(shù)據(jù)庫中某個(gè)字段值是否有效,是否符合預(yù)期值。傳統(tǒng)的做法可能充斥著大量相似的查詢邏輯,每次都需要手動構(gòu)建查詢條件、執(zhí)行查詢并處理結(jié)果,這樣的代碼既冗長又難以維護(hù)。

例如以下兩個(gè)驗(yàn)證用戶 ID 和部門 ID 是否有效的方法,雖然簡單,但每次需要校驗(yàn)不同實(shí)體或不同條件時(shí),就需要復(fù)制粘貼并做相應(yīng)修改,導(dǎo)致代碼庫中充滿了大量雷同的校驗(yàn)邏輯,給維護(hù)帶來了困擾。

//?判斷用戶?ID?是否有效

publicvoidcheckUserExistence(String userId){

User user?=?userDao.findById(userId);

if(user?==null)?{

thrownewRuntimeException("用戶ID無效");

}

}

//?判斷部門?ID?是否有效

publicvoidcheckDeptExistence(String deptId){

Dept dept?=?deptDao.findById(deptId);

if(dept?==null)?{

thrownewRuntimeException("部門ID無效");

}

}

Java 8 的魔法棒:函數(shù)式接口

Java 8 引入了函數(shù)式接口的概念,其中?Function<T, R>?是最基礎(chǔ)的代表,它接受一個(gè)類型?T?的輸入,返回類型?R?的結(jié)果。而在?MyBatis Plus?等框架中常用的?SFunction?是對?Lambda表達(dá)式的進(jìn)一步封裝,使得我們可以更加靈活地操作實(shí)體類的屬性。

實(shí)戰(zhàn)演練:重構(gòu)斷言方法

下面的ensureColumnValueValid方法正是利用了函數(shù)式接口的魅力,實(shí)現(xiàn)了對任意實(shí)體類指定列值的有效性斷言:

/**

*?確認(rèn)數(shù)據(jù)庫字段值有效(通用)

*

*@param?待驗(yàn)證值的類型

*@paramvalueToCheck?待驗(yàn)證的值

*@paramcolumnExtractor?實(shí)體類屬性提取函數(shù)

*@paramqueryExecutor?單條數(shù)據(jù)查詢執(zhí)行器

*@paramerrorMessage?異常提示信息模板

*/

publicstaticvoidensureColumnValueValid(V valueToCheck,?SFunction<T,?R>?columnExtractor,?SFunction<LambdaQueryWrapper<T>,?T>?queryExecutor,?String errorMessage){

if(valueToCheck?==null)return;

LambdaQueryWrapper?wrapper?=newLambdaQueryWrapper<>();

wrapper.select(columnExtractor);

wrapper.eq(columnExtractor,?valueToCheck);

wrapper.last("LIMIT 1");

T entity?=?queryExecutor.apply(wrapper);

R columnValue?=?columnExtractor.apply(entity);

if(entity?==null||?columnValue?==null)

thrownewDataValidationException(String.format(errorMessage,?valueToCheck));

}

這個(gè)方法接受一個(gè)待驗(yàn)證的值、一個(gè)實(shí)體類屬性提取函數(shù)、一個(gè)單行數(shù)據(jù)查詢執(zhí)行器和一個(gè)異常信息模板作為參數(shù)。通過這四個(gè)參數(shù),不僅能夠進(jìn)行針對特定屬性的有效性檢查,而且還能生成具有一致性的異常信息。

對比分析

使用?Function?改造前

//?判斷用戶?ID?是否有效

publicvoidcheckUserExistence(String userId){

User user?=?userDao.findById(userId);

if(user?==null)?{

thrownewRuntimeException("用戶ID無效");

}

}

//?判斷部門?ID?是否有效

publicvoidcheckDeptExistence(String deptId){

Dept dept?=?deptDao.findById(deptId);

if(dept?==null)?{

thrownewRuntimeException("部門ID無效");

}

}

使用?Function?改造后

publicvoidassignTaskToUser(AddOrderDTO dto){

ensureColumnValueValid(dto.getUserId(),?User::getId,?userDao::getOne,"用戶ID無效");

ensureColumnValueValid(dto.getDeptId(),?Dept::getId,?deptDao::getOne,"部門ID無效");

ensureColumnValueValid(dto.getCustomerId(),?Customer::getId,?customerDao::getOne,"客戶ID無效");

ensureColumnValueValid(dto.getDeptId(),?Dept::getId,?deptDao::getOne,"部門ID無效");

ensureColumnValueValid(dto.getSupplieId(),?Supplie::getId,?supplierDao::getOne,"供應(yīng)商ID無效");

//?現(xiàn)在可以確信客戶存在

Customer cus?=?customerDao.findById(dto.getCustomerId());

//?創(chuàng)建訂單的邏輯...

}

對比上述兩段代碼,我們發(fā)現(xiàn)后者不僅大幅減少了代碼量,而且通過函數(shù)式編程,表達(dá)出更為清晰的邏輯意圖,可讀性和可維護(hù)性都有所提高。

優(yōu)點(diǎn)

減少重復(fù)代碼:通過ensureColumnValueValid方法,所有涉及數(shù)據(jù)庫字段值有效性檢查的地方都可以復(fù)用相同的邏輯,將變化的部分作為參數(shù)傳遞,大大減少了因特定校驗(yàn)邏輯而產(chǎn)生的代碼量。

增強(qiáng)代碼復(fù)用:抽象化的校驗(yàn)方法適用于多種場景,無論是用戶ID、訂單號還是其他任何實(shí)體屬性的校驗(yàn),一套邏輯即可應(yīng)對。

提升可讀性和維護(hù)性:通過清晰的函數(shù)簽名和 Lambda 表達(dá)式,代碼意圖一目了然,降低了后續(xù)維護(hù)的成本。

靈活性和擴(kuò)展性:當(dāng)校驗(yàn)規(guī)則發(fā)生變化時(shí),只需要調(diào)整ensureColumnValueValid方法或其內(nèi)部實(shí)現(xiàn),所有調(diào)用該方法的地方都會自動受益,提高了系統(tǒng)的靈活性和擴(kuò)展性。

舉一反三:拓展校驗(yàn)邏輯的邊界

通過上述的實(shí)踐,我們見識到了函數(shù)式編程在簡化數(shù)據(jù)校驗(yàn)邏輯方面的威力。但這只是冰山一角,我們可以根據(jù)不同的業(yè)務(wù)場景,繼續(xù)擴(kuò)展和完善校驗(yàn)邏輯,實(shí)現(xiàn)更多樣化的校驗(yàn)需求。以下兩個(gè)示例展示了如何在原有基礎(chǔ)上進(jìn)一步深化,實(shí)現(xiàn)更復(fù)雜的數(shù)據(jù)比較和驗(yàn)證功能。

斷言指定列值等于預(yù)期值

首先,考慮一個(gè)場景:除了驗(yàn)證數(shù)據(jù)的存在性,我們還需確認(rèn)查詢到的某列值是否與預(yù)期值相符。這在驗(yàn)證用戶角色、狀態(tài)變更等場景中尤為常見。為此,我們設(shè)計(jì)了validateColumnValueMatchesExpected方法:

/**

*?驗(yàn)證查詢結(jié)果中指定列的值是否與預(yù)期值匹配

*

*@param?????????????實(shí)體類型

*@param?????????????目標(biāo)列值的類型

*@param?????????????查詢條件列值的類型

*@paramtargetColumn????目標(biāo)列的提取函數(shù),用于獲取想要驗(yàn)證的列值

*@paramexpectedValue???期望的列值

*@paramconditionColumn?條件列的提取函數(shù),用于設(shè)置查詢條件

*@paramconditionValue??條件列對應(yīng)的值

*@paramqueryMethod?????執(zhí)行查詢的方法引用,返回單個(gè)實(shí)體對象

*@paramerrorMessage????驗(yàn)證失敗時(shí)拋出異常的錯(cuò)誤信息模板

*@throwsRuntimeException?當(dāng)查詢結(jié)果中目標(biāo)列的值與預(yù)期值不匹配時(shí)拋出異常

*/

publicstaticvoidvalidateColumnValueMatchesExpected(

SFunction?targetColumn,?R expectedValue,

SFunction?conditionColumn,?C conditionValue,

SFunction,?T>?queryMethod,

String errorMessage){

//?創(chuàng)建查詢包裝器,選擇目標(biāo)列并設(shè)置查詢條件

LambdaQueryWrapper?wrapper?=newLambdaQueryWrapper<>();

wrapper.select(targetColumn);

wrapper.eq(conditionColumn,?conditionValue);

//?執(zhí)行查詢方法

T one?=?queryMethod.apply(wrapper);

//?如果查詢結(jié)果為空,則直接返回,視為驗(yàn)證通過(或忽略)

if(one?==null)return;

//?獲取查詢結(jié)果中目標(biāo)列的實(shí)際值

R actualValue?=?targetColumn.apply(one);

//?比較實(shí)際值與預(yù)期值是否匹配,這里假設(shè)notMatch是一個(gè)自定義方法用于比較不匹配情況

booleandoesNotMatch?=?notMatch(actualValue,?expectedValue);

if(doesNotMatch)?{

//?若不匹配,則根據(jù)錯(cuò)誤信息模板拋出異常

thrownewRuntimeException(String.format(errorMessage,?expectedValue,?actualValue));

}

}

//?假設(shè)的輔助方法,用于比較值是否不匹配,根據(jù)實(shí)際需要實(shí)現(xiàn)

privatestaticbooleannotMatch(R actual,?R expected){

//?示例簡單實(shí)現(xiàn)為不相等判斷,實(shí)際情況可能更復(fù)雜

return!Objects.equals(actual,?expected);

}

這個(gè)方法允許我們指定一個(gè)查詢目標(biāo)列(targetColumn)、預(yù)期值(expectedValue)、查詢條件列(conditionColumn)及其對應(yīng)的條件值(conditionValue),并提供一個(gè)查詢方法(queryMethod)來執(zhí)行查詢。如果查詢到的列值與預(yù)期不符,則拋出異常,錯(cuò)誤信息通過 errorMessage 參數(shù)定制。

應(yīng)用場景:例如在一個(gè)權(quán)限管理系統(tǒng)中,當(dāng)需要更新用戶角色時(shí),系統(tǒng)需要確保當(dāng)前用戶的角色在更新前是 “普通用戶”,才能將其升級為 “管理員”。此場景下,可以使用validateColumnValueMatchesExpected方法來驗(yàn)證用戶當(dāng)前的角色是否確實(shí)為“普通用戶”。

//?當(dāng)用戶角色不是?“普通用戶”?時(shí)拋異常

validateColumnValueMatchesExpected(User::getRoleType,"普通用戶",?User::getId,?userId,?userMapper::getOne,"用戶角色不是普通用戶,無法升級為管理員!");

斷言指定值位于期望值列表內(nèi)

進(jìn)一步,某些情況下我們需要驗(yàn)證查詢結(jié)果中的某一列值是否屬于一個(gè)預(yù)設(shè)的值集合。例如,驗(yàn)證用戶角色是否合法。為此,我們創(chuàng)建了validateColumnValueMatchesExpectedList方法:

/**

*?驗(yàn)證查詢結(jié)果中指定列的值是否位于預(yù)期值列表內(nèi)

*

*@param?????????????實(shí)體類型

*@param?????????????目標(biāo)列值的類型

*@param?????????????查詢條件列值的類型

*@paramtargetColumn????目標(biāo)列的提取函數(shù),用于獲取想要驗(yàn)證的列值

*@paramexpectedValueList?期望值的列表

*@paramconditionColumn?條件列的提取函數(shù),用于設(shè)置查詢條件

*@paramconditionValue??條件列對應(yīng)的值

*@paramqueryMethod?????執(zhí)行查詢的方法引用,返回單個(gè)實(shí)體對象

*@paramerrorMessage????驗(yàn)證失敗時(shí)拋出異常的錯(cuò)誤信息模板

*@throwsRuntimeException?當(dāng)查詢結(jié)果中目標(biāo)列的值不在預(yù)期值列表內(nèi)時(shí)拋出異常

*/

publicstaticvoidvalidateColumnValueInExpectedList(

SFunction?targetColumn,?List?expectedValueList,

SFunction?conditionColumn,?C conditionValue,

SFunction,?T>?queryMethod,

String errorMessage){

LambdaQueryWrapper?wrapper?=newLambdaQueryWrapper<>();

wrapper.select(targetColumn);

wrapper.eq(conditionColumn,?conditionValue);

T one?=?queryMethod.apply(wrapper);

if(one?==null)return;

R actualValue?=?targetColumn.apply(one);

if(actualValue?==null)thrownewRuntimeException("列查詢結(jié)果為空");

if(!expectedValueList.contains(actualValue))?{

thrownewRuntimeException(errorMessage);

}

}

這個(gè)方法接受一個(gè)目標(biāo)列(targetColumn)、一個(gè)預(yù)期值列表(expectedValueList)、查詢條件列(conditionColumn)及其條件值(conditionValue),同樣需要一個(gè)查詢方法(queryMethod)。如果查詢到的列值不在預(yù)期值列表中,則觸發(fā)異常。

應(yīng)用場景:在一個(gè)電商平臺的訂單處理流程中,系統(tǒng)需要驗(yàn)證訂單狀態(tài)是否處于可取消的狀態(tài)列表里(如 “待支付”、“待發(fā)貨”)才允許用戶取消訂單。此時(shí),validateColumnValueInExpectedList方法能有效確保操作的合法性。

//?假設(shè)?OrderStatusEnum?枚舉了所有可能的訂單狀態(tài),cancelableStatuses?包含可取消的狀態(tài)

List?cancelableStatuses?=?Arrays.asList(OrderStatusEnum.WAITING_PAYMENT.getValue(),?OrderStatusEnum.WAITING_DELIVERY.getValue());

//?驗(yàn)證訂單狀態(tài)是否在可取消狀態(tài)列表內(nèi)

validateColumnValueInExpectedList(Order::getStatus,?cancelableStatuses,?Order::getOrderId,?orderId,?orderMapper::selectOne,"訂單當(dāng)前狀態(tài)不允許取消!");

通過這兩個(gè)擴(kuò)展方法,我們不僅鞏固了函數(shù)式編程在減少代碼重復(fù)、提升代碼靈活性方面的優(yōu)勢,還進(jìn)一步證明了通過抽象和泛型設(shè)計(jì),可以輕松應(yīng)對各種復(fù)雜的業(yè)務(wù)校驗(yàn)需求,使代碼更加貼近業(yè)務(wù)邏輯,易于理解和維護(hù)。

核心優(yōu)勢

代碼復(fù)用:通過泛型和函數(shù)式接口,該方法能夠適應(yīng)任何實(shí)體類和屬性的校驗(yàn)需求,大大減少了重復(fù)的查詢邏輯代碼。

清晰表達(dá)意圖:方法簽名直觀表達(dá)了校驗(yàn)邏輯的目的,提高了代碼的可讀性和可維護(hù)性。

靈活性:使用者只需提供幾個(gè)簡單的 Lambda 表達(dá)式,即可完成復(fù)雜的查詢邏輯配置,無需關(guān)心底層實(shí)現(xiàn)細(xì)節(jié)。

易于維護(hù)與擴(kuò)展:

當(dāng)需要增加新的實(shí)體驗(yàn)證時(shí),僅需調(diào)用ensureColumnValueValid并傳入相應(yīng)的參數(shù),無需編寫新的驗(yàn)證邏輯,降低了維護(hù)成本。

修改驗(yàn)證規(guī)則時(shí),只需調(diào)整ensureColumnValueValid內(nèi)部實(shí)現(xiàn),所有調(diào)用處自動遵循新規(guī)則,便于統(tǒng)一管理。

異常處理集中于ensureColumnValueValid方法內(nèi)部,統(tǒng)一了異常拋出行為,避免了在多個(gè)地方處理相同的邏輯錯(cuò)誤,減少了潛在的錯(cuò)誤源。

函數(shù)式編程的力量

通過這個(gè)實(shí)例,我們見證了函數(shù)式編程在簡化代碼、提高抽象層次上的強(qiáng)大能力。在 Java 8 及之后的版本中,擁抱函數(shù)式編程思想,不僅能夠使我們的代碼更加簡潔、靈活,還能在一定程度上促進(jìn)代碼的正確性和可測試性。因此,無論是日常開發(fā)還是系統(tǒng)設(shè)計(jì),都值得我們深入探索和應(yīng)用這一現(xiàn)代編程范式,讓代碼如魔法般優(yōu)雅而高效。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評論 2 374

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