利用Function介面告別冗餘(屎山)程式碼

原文:juejin.cn/post/7384256110280572980
  • 前言
  • 背景故事:資料校驗的煩惱
  • Java 8 的魔法棒:函式式介面
  • 實戰演練:重構斷言方法
  • 對比分析
  • 舉一反三:拓展校驗邏輯的邊界
  • 核心優勢
  • 函數語言程式設計的力量

前言

Java 開發的征途中,我們時常與重複程式碼不期而遇。這些重複程式碼不僅讓專案顯得笨重,更增加了維護成本。幸運的是,Java 8 帶來了函數語言程式設計的春風,以 Function 介面為代表的一系列新特性,為我們提供了破除這一難題的利劍。
本文將以一個實際應用場景為例,即使用 Java 8 的函數語言程式設計特性來重構資料有效性斷言邏輯,展示如何透過 SFunction(基於 Java 8Lambda 表示式封裝)減少程式碼重複,從而提升程式碼的優雅性和可維護性。

背景故事:資料校驗的煩惱

想象一下,在一個複雜的業務系統中,我們可能需要頻繁地驗證資料庫中某個欄位值是否有效,是否符合預期值。傳統的做法可能充斥著大量相似的查詢邏輯,每次都需要手動構建查詢條件、執行查詢並處理結果,這樣的程式碼既冗長又難以維護。
例如以下兩個驗證使用者 ID 和部門 ID 是否有效的方法,雖然簡單,但每次需要校驗不同實體或不同條件時,就需要複製貼上並做相應修改,導致程式碼庫中充滿了大量雷同的校驗邏輯,給維護帶來了困擾。

// 判斷使用者 ID 是否有效

public void checkUserExistence(String userId) {

    User user = userDao.findById(userId);

if

 (user == null) {

        throw new RuntimeException(

"使用者ID無效"

);

    }

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

public void checkDeptExistence(String deptId) {

    Dept dept = deptDao.findById(deptId);

if

 (dept == null) {

        throw new RuntimeException(

"部門ID無效"

);

    }   

}

Java 8 的魔法棒:函式式介面

Java 8 引入了函式式介面的概念,其中 Function<T, R> 是最基礎的代表,它接受一個型別 T 的輸入,返回型別 R 的結果。而在 MyBatis Plus 等框架中常用的 SFunction 是對 Lambda 表示式的進一步封裝,使得我們可以更加靈活地操作實體類的屬性。

實戰演練:重構斷言方法

下面的 ensureColumnValueValid 方法正是利用了函式式介面的魅力,實現了對任意實體類指定列值的有效性斷言:

/**

 * 確認資料庫欄位值有效(通用)

 * 

 * @param <V> 待驗證值的型別

 * @param valueToCheck 待驗證的值

 * @param columnExtractor 實體類屬性提取函式

 * @param queryExecutor 單條資料查詢執行器

 * @param errorMessage 異常提示資訊模板

 */

public static <T, R, V> void ensureColumnValueValid(V valueToCheck, SFunction<T, R> columnExtractor, SFunction<LambdaQueryWrapper<T>, T> queryExecutor, String errorMessage) {

if

 (valueToCheck == null) 

return

;
    LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();

    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)

        throw new DataValidationException(String.format(errorMessage, valueToCheck));

}

這個方法接受一個待驗證的值、一個實體類屬性提取函式、一個單行資料查詢執行器和一個異常資訊模板作為引數。透過這四個引數,不僅能夠進行針對特定屬性的有效性檢查,而且還能生成具有一致性的異常資訊。

對比分析

使用 Function 改造前

// 判斷使用者 ID 是否有效

public void checkUserExistence(String userId) {

    User user = userDao.findById(userId);

if

 (user == null) {

        throw new RuntimeException(

"使用者ID無效"

);

    }

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

public void checkDeptExistence(String deptId) {

    Dept dept = deptDao.findById(deptId);

if

 (dept == null) {

        throw new RuntimeException(

"部門ID無效"

);

    }   

}

使用 Function 改造後

public void assignTaskToUser(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, 

"供應商ID無效"

);
    // 現在可以確信客戶存在

    Customer cus = customerDao.findById(dto.getCustomerId());     
    // 建立訂單的邏輯...

}

對比上述兩段程式碼,我們發現後者不僅大幅減少了程式碼量,而且透過函數語言程式設計,表達出更為清晰的邏輯意圖,可讀性和可維護性都有所提高。
優點
  1. 減少重複程式碼: 透過 ensureColumnValueValid 方法,所有涉及資料庫欄位值有效性檢查的地方都可以複用相同的邏輯,將變化的部分作為引數傳遞,大大減少了因特定校驗邏輯而產生的程式碼量。
  2. 增強程式碼複用: 抽象化的校驗方法適用於多種場景,無論是使用者ID、訂單號還是其他任何實體屬性的校驗,一套邏輯即可應對。
  3. 提升可讀性和維護性: 透過清晰的函式簽名和 Lambda 表示式,程式碼意圖一目瞭然,降低了後續維護的成本。
  4. 靈活性和擴充套件性: 當校驗規則發生變化時,只需要調整 ensureColumnValueValid 方法或其內部實現,所有呼叫該方法的地方都會自動受益,提高了系統的靈活性和擴充套件性。

舉一反三:拓展校驗邏輯的邊界

透過上述的實踐,我們見識到了函數語言程式設計在簡化資料校驗邏輯方面的威力。但這只是冰山一角,我們可以根據不同的業務場景,繼續擴充套件和完善校驗邏輯,實現更多樣化的校驗需求。以下兩個示例展示瞭如何在原有基礎上進一步深化,實現更復雜的資料比較和驗證功能。
斷言指定列值等於預期值
首先,考慮一個場景:除了驗證資料的存在性,我們還需確認查詢到的某列值是否與預期值相符。這在驗證使用者角色、狀態變更等場景中尤為常見。為此,我們設計了 validateColumnValueMatchesExpected 方法:

/**

 * 驗證查詢結果中指定列的值是否與預期值匹配

 *

 * @param <T>             實體型別

 * @param <R>             目標列值的型別

 * @param <C>             查詢條件列值的型別

 * @param targetColumn    目標列的提取函式,用於獲取想要驗證的列值

 * @param expectedValue   期望的列值

 * @param conditionColumn 條件列的提取函式,用於設定查詢條件

 * @param conditionValue  條件列對應的值

 * @param queryMethod     執行查詢的方法引用,返回單個實體物件

 * @param errorMessage    驗證失敗時丟擲異常的錯誤資訊模板

 * @throws RuntimeException 當查詢結果中目標列的值與預期值不匹配時丟擲異常

 */

public static <T, R, C> void validateColumnValueMatchesExpected(

      SFunction<T, R> targetColumn, R expectedValue,

      SFunction<T, C> conditionColumn, C conditionValue,

      SFunction<LambdaQueryWrapper<T>, T> queryMethod,

      String errorMessage) {
   // 建立查詢包裝器,選擇目標列並設定查詢條件

   LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();

   wrapper.select(targetColumn);

   wrapper.eq(conditionColumn, conditionValue);
   // 執行查詢方法

   T one = queryMethod.apply(wrapper);

   // 如果查詢結果為空,則直接返回,視為驗證透過(或忽略)

if

 (one == null) 

return

;
   // 獲取查詢結果中目標列的實際值

   R actualValue = targetColumn.apply(one);
   // 比較實際值與預期值是否匹配,這裡假設notMatch是一個自定義方法用於比較不匹配情況

   boolean doesNotMatch = notMatch(actualValue, expectedValue);

if

 (doesNotMatch) {

      // 若不匹配,則根據錯誤資訊模板丟擲異常

      throw new RuntimeException(String.format(errorMessage, expectedValue, actualValue));

   }

}
// 假設的輔助方法,用於比較值是否不匹配,根據實際需要實現

private static <R> boolean notMatch(R actual, R expected) {

    // 示例簡單實現為不相等判斷,實際情況可能更復雜

return

 !Objects.equals(actual, expected);

}

這個方法允許我們指定一個查詢目標列(targetColumn)、預期值(expectedValue)、查詢條件列(conditionColumn)及其對應的條件值(conditionValue),並提供一個查詢方法(queryMethod)來執行查詢。如果查詢到的列值與預期不符,則丟擲異常,錯誤資訊透過 errorMessage 引數定製。
應用場景: 例如在一個許可權管理系統中,當需要更新使用者角色時,系統需要確保當前使用者的角色在更新前是 “普通使用者”,才能將其升級為 “管理員”。此場景下,可以使用 validateColumnValueMatchesExpected 方法來驗證使用者當前的角色是否確實為“普通使用者”。

// 當用戶角色不是 “普通使用者” 時拋異常

validateColumnValueMatchesExpected(User::getRoleType, 

"普通使用者"

, User::getId, userId, userMapper::getOne, 

"使用者角色不是普通使用者,無法升級為管理員!"

);

斷言指定值位於期望值列表內
進一步,某些情況下我們需要驗證查詢結果中的某一列值是否屬於一個預設的值集合。例如,驗證使用者角色是否合法。為此,我們建立了 validateColumnValueMatchesExpectedList 方法:

/**

 * 驗證查詢結果中指定列的值是否位於預期值列表內

 *

 * @param <T>             實體型別

 * @param <R>             目標列值的型別

 * @param <C>             查詢條件列值的型別

 * @param targetColumn    目標列的提取函式,用於獲取想要驗證的列值

 * @param expectedValueList 期望值的列表

 * @param conditionColumn 條件列的提取函式,用於設定查詢條件

 * @param conditionValue  條件列對應的值

 * @param queryMethod     執行查詢的方法引用,返回單個實體物件

 * @param errorMessage    驗證失敗時丟擲異常的錯誤資訊模板

 * @throws RuntimeException 當查詢結果中目標列的值不在預期值列表內時丟擲異常

 */

public static <T, R, C> void validateColumnValueInExpectedList(

        SFunction<T, R> targetColumn, List<R> expectedValueList,

        SFunction<T, C> conditionColumn, C conditionValue,

        SFunction<LambdaQueryWrapper<T>, T> queryMethod,

        String errorMessage) {
    LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();

    wrapper.select(targetColumn);

    wrapper.eq(conditionColumn, conditionValue);
    T one = queryMethod.apply(wrapper);

if

 (one == null) 

return

;
    R actualValue = targetColumn.apply(one);

if

 (actualValue == null) throw new RuntimeException(

"列查詢結果為空"

);

if

 (!expectedValueList.contains(actualValue)) {        

        throw new RuntimeException(errorMessage);

    }

}

這個方法接受一個目標列(targetColumn)、一個預期值列表(expectedValueList)、查詢條件列(conditionColumn)及其條件值(conditionValue),同樣需要一個查詢方法(queryMethod)。如果查詢到的列值不在預期值列表中,則觸發異常。
應用場景: 在一個電商平臺的訂單處理流程中,系統需要驗證訂單狀態是否處於可取消的狀態列表裡(如 “待支付”、“待發貨”)才允許使用者取消訂單。此時,validateColumnValueInExpectedList 方法能有效確保操作的合法性。

// 假設 OrderStatusEnum 枚舉了所有可能的訂單狀態,cancelableStatuses 包含可取消的狀態

List<String> cancelableStatuses = Arrays.asList(OrderStatusEnum.WAITING_PAYMENT.getValue(), OrderStatusEnum.WAITING_DELIVERY.getValue());
// 驗證訂單狀態是否在可取消狀態列表內

validateColumnValueInExpectedList(Order::getStatus, cancelableStatuses, Order::getOrderId, orderId, orderMapper::selectOne, 

"訂單當前狀態不允許取消!"

);

透過這兩個擴充套件方法,我們不僅鞏固了函數語言程式設計在減少程式碼重複、提升程式碼靈活性方面的優勢,還進一步證明了透過抽象和泛型設計,可以輕鬆應對各種複雜的業務校驗需求,使程式碼更加貼近業務邏輯,易於理解和維護。

核心優勢

  • 程式碼複用 :透過泛型和函式式介面,該方法能夠適應任何實體類和屬性的校驗需求,大大減少了重複的查詢邏輯程式碼。
  • 清晰表達意圖 :方法簽名直觀表達了校驗邏輯的目的,提高了程式碼的可讀性和可維護性。
  • 靈活性 :使用者只需提供幾個簡單的 Lambda 表示式,即可完成複雜的查詢邏輯配置,無需關心底層實現細節。
  • 易於維護與擴充套件:
    • 異常處理集中於 ensureColumnValueValid 方法內部,統一了異常丟擲行為,避免了在多個地方處理相同的邏輯錯誤,減少了潛在的錯誤源。
    • 修改驗證規則時,只需調整 ensureColumnValueValid 內部實現,所有呼叫處自動遵循新規則,便於統一管理。
    • 當需要增加新的實體驗證時,僅需呼叫 ensureColumnValueValid 並傳入相應的引數,無需編寫新的驗證邏輯,降低了維護成本。

函數語言程式設計的力量

透過這個例項,我們見證了函數語言程式設計在簡化程式碼、提高抽象層次上的強大能力。在 Java 8 及之後的版本中,擁抱函數語言程式設計思想,不僅能夠使我們的程式碼更加簡潔、靈活,還能在一定程度上促進程式碼的正確性和可測試性。
因此,無論是日常開發還是系統設計,都值得我們深入探索和應用這一現代程式設計正規化,讓程式碼如魔法般優雅而高效。
END
官方站點:www.linuxprobe.com
Linux命令大全:www.linuxcool.com
劉遄老師QQ:5604215
Linux技術交流群:2636170
(新群,火熱加群中……)
想要學習Linux系統的讀者可以點選"閱讀原文"按鈕來了解書籍《Linux就該這麼學》,同時也非常適合專業的運維人員閱讀,成為輔助您工作的高價值工具書!


相關文章