Bootstrap

利用 Function 接口告别冗余(屎山)代码

前言

在 ​​Java​​​ 开发的征途中,我们时常与重复代码不期而遇。这些重复代码不仅让项目显得笨重,更增加了维护成本。幸运的是,​​Java 8​​​ 带来了函数式编程的春风,以 ​​Function​​​ 接口为代表的一系列新特性,为我们提供了破除这一难题的利剑。本文将以一个实际应用场景为例,即使用 ​​Java 8​​​ 的函数式编程特性来重构数据有效性断言逻辑,展示如何通过 ​​SFunction​​​(基于 ​​Java 8​​​ 的 ​​Lambda​​ 表达式封装)减少代码重复,从而提升代码的优雅性和可维护性。​

背景故事:数据校验的烦恼

想象一下,在一个复杂的业务系统中,我们可能需要频繁地验证数据库中某个字段值是否有效,是否符合预期值。传统的做法可能充斥着大量相似的查询逻辑,每次都需要手动构建查询条件、执行查询并处理结果,这样的代码既冗长又难以维护。

例如以下两个验证用户 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, "订单当前状态不允许取消!");

通过这两个扩展方法,我们不仅巩固了函数式编程在减少代码重复、提升代码灵活性方面的优势,还进一步证明了通过抽象和泛型设计,可以轻松应对各种复杂的业务校验需求,使代码更加贴近业务逻辑,易于理解和维护。

核心优势

  1. 代码复用:通过泛型和函数式接口,该方法能够适应任何实体类和属性的校验需求,大大减少了重复的查询逻辑代码。
  2. 清晰表达意图:方法签名直观表达了校验逻辑的目的,提高了代码的可读性和可维护性。
  3. 灵活性:使用者只需提供几个简单的 Lambda 表达式,即可完成复杂的查询逻辑配置,无需关心底层实现细节。
  4. 易于维护与扩展:
  • 当需要增加新的实体验证时,仅需调用 ​​ensureColumnValueValid​​ 并传入相应的参数,无需编写新的验证逻辑,降低了维护成本。
  • 修改验证规则时,只需调整 ​​ensureColumnValueValid​​ 内部实现,所有调用处自动遵循新规则,便于统一管理。
  • 异常处理集中于 ​​ensureColumnValueValid​​ 方法内部,统一了异常抛出行为,避免了在多个地方处理相同的逻辑错误,减少了潜在的错误源。

函数式编程的力量

通过这个实例,我们见证了函数式编程在简化代码、提高抽象层次上的强大能力。在 Java 8 及之后的版本中,拥抱函数式编程思想,不仅能够使我们的代码更加简洁、灵活,还能在一定程度上促进代码的正确性和可测试性。因此,无论是日常开发还是系统设计,都值得我们深入探索和应用这一现代编程范式,让代码如魔法般优雅而高效。

;