在
Java
开发的征途中,我们时常与重复代码不期而遇。这些重复代码不仅让项目显得笨重,更增加了维护成本。幸运的是,
Java 8
带来了函数式编程的春风,以
Function
接口为代表的一系列新特性,为我们提供了破除这一难题的利剑。
本文将以一个实际应用场景为例,即使用
Java 8
的函数式编程特性来重构数据有效性断言逻辑,展示如何通过
SFunction
(基于
Java 8
的
Lambda
表达式封装)减少代码重复,从而提升代码的优雅性和可维护性。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
想象一下,在一个复杂的业务系统中,我们可能需要频繁地验证数据库中某个字段值是否有效,是否符合预期值。传统的做法可能充斥着大量相似的查询逻辑,每次都需要手动构建查询条件、执行查询并处理结果,这样的代码既冗长又难以维护。
例如以下两个验证用户 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无效");
}
}
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
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));
}
这个方法接受一个待验证的值、一个实体类属性提取函数、一个单行数据查询执行器和一个异常信息模板作为参数。通过这四个参数,不仅能够进行针对特定属性的有效性检查,而且还能生成具有一致性的异常信息。
// 判断用户 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无效"
);
}
}
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
参数定制。
应用场景:
例如在一个权限管理系统中,当需要更新用户角色时,系统需要确保当前用户的角色在更新前是 “普通用户”,才能将其升级为 “管理员”。此场景下,可以使用
validateColumnValueMatchesExpected
方法来验证用户当前的角色是否确实为“普通用户”。
// 当用户角色不是 “普通用户” 时抛异常
validateColumnValueMatchesExpected(User::getRoleType, "普通用户", User::getId, userId, userMapper::getOne, "用户角色不是普通用户,无法升级为管理员!");
进一步,某些情况下我们需要验证查询结果中的某一列值是否属于一个预设的值集合。例如,验证用户角色是否合法。为此,我们创建了
validateColumnValueMatchesExpectedList
方法:
/**
* 验证查询结果中指定列的值是否位于预期值列表内
*
* @param 实体类型
* @param 目标列值的类型
* @param 查询条件列值的类型
* @param targetColumn 目标列的提取函数,用于获取想要验证的列值
* @param expectedValueList 期望值的列表
* @param conditionColumn 条件列的提取函数,用于设置查询条件
* @param conditionValue 条件列对应的值
* @param queryMethod 执行查询的方法引用,返回单个实体对象
* @param errorMessage 验证失败时抛出异常的错误信息模板
* @throws RuntimeException 当查询结果中目标列的值不在预期值列表内时抛出异常
*/
public static void validateColumnValueInExpectedList(
SFunction targetColumn, List expectedValueList,
SFunction conditionColumn, C conditionValue,
SFunction, T> queryMethod,
String errorMessage) {
LambdaQueryWrapper wrapper = new