前言
在 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 的结果。而在 MyBatis Plus 等框架中常用的
SFunction
是对
Lambda
表达式的进一步封装,使得我们可以更加灵活地操作实体类的属性。
实战演练:重构断言方法
下面的
ensureColumnValueValid
方法正是利用了函数式接口的魅力,实现了对任意实体类指定列值的有效性断言:
/**
* 确认数据库字段值有效(通用)
*
* @param 待验证值的类型
* @param valueToCheck 待验证的值
* @param columnExtractor 实体类属性提取函数
* @param queryExecutor 单条数据查询执行器
* @param errorMessage 异常提示信息模板
*/
public static void ensureColumnValueValid(V valueToCheck, SFunction columnExtractor, SFunction, T> queryExecutor, String errorMessage) {
if (valueToCheck == null) return;
LambdaQueryWrapper 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());
// 创建订单的逻辑...
}
对比上述两段代码,我们发现后者不仅大幅减少了代码量,而且通过函数式编程,表达出更为清晰的逻辑意图,可读性和可维护性都有所提高。
优点
-
减少重复代码:
通过
ensureColumnValueValid
方法,所有涉及数据库字段值有效性检查的地方都可以复用相同的逻辑,将变化的部分作为参数传递,大大减少了因特定校验逻辑而产生的代码量。
-
增强代码复用:
抽象化的校验方法适用于多种场景,无论是用户ID、订单号还是其他任何实体属性的校验,一套逻辑即可应对。
-
提升可读性和维护性:
通过清晰的函数签名和 Lambda 表达式,代码意图一目了然,降低了后续维护的成本。
-
灵活性和扩展性:
当校验规则发生变化时,只需要调整
ensureColumnValueValid
方法或其内部实现,所有调用该方法的地方都会自动受益,提高了系统的灵活性和扩展性。
举一反三:拓展校验逻辑的边界
通过上述的实践,我们见识到了函数式编程在简化数据校验逻辑方面的威力。但这只是冰山一角,我们可以根据不同的业务场景,继续扩展和完善校验逻辑,实现更多样化的校验需求。以下两个示例展示了如何在原有基础上进一步深化,实现更复杂的数据比较和验证功能。
断言指定列值等于预期值
首先,考虑一个场景:除了验证数据的存在性,我们还需确认查询到的某列值是否与预期值相符。这在验证用户角色、状态变更等场景中尤为常见。为此,我们设计了
validateColumnValueMatchesExpected
方法:
/**
* 验证查询结果中指定列的值是否与预期值匹配
*
* @param 实体类型
* @param 目标列值的类型
* @param 查询条件列值的类型
* @param targetColumn 目标列的提取函数,用于获取想要验证的列值
* @param expectedValue 期望的列值
* @param conditionColumn 条件列的提取函数,用于设置查询条件
* @param conditionValue 条件列对应的值
* @param queryMethod 执行查询的方法引用,返回单个实体对象
* @param errorMessage 验证失败时抛出异常的错误信息模板
* @throws RuntimeException 当查询结果中目标列的值与预期值不匹配时抛出异常
*/
public static void validateColumnValueMatchesExpected(
SFunction targetColumn, R expectedValue,
SFunction conditionColumn, C conditionValue,
SFunction, T> queryMethod,
String errorMessage) {
// 创建查询包装器,选择目标列并设置查询条件
LambdaQueryWrapper 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 boolean notMatch(R actual, R expected) {
// 示例简单实现为不相等判断,实际情况可能更复杂
return !Objects.equals(actual, expected);
}
这个方法允许我们指定一个查询目标列(
targetColumn
)、预期值(
expectedValue
)、查询条件列(
conditionColumn
)及其对应的条件值(
conditionValue
),并提供一个查询方法(
queryMethod
)来执行查询。如果查询到的列值与预期不符,则抛出异常,错误信息通过
errorMessage
参数定制。