专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
51好读  ›  专栏  ›  Java基基

函数式编程利器:Java 8 Function 提升断言效率

Java基基  · 公众号  ·  · 2024-10-30 11:55

主要观点总结

文章介绍了函数式编程在Java开发中的应用,通过实例展示了如何使用Java 8的魔法棒——函数式接口来重构数据有效性断言逻辑。文章介绍了两个方法ensureColumnValueValid和validateColumnValueMatchesExpected,用于减少重复代码和提高代码的可读性和可维护性。文章还讨论了函数式编程的优势,如代码复用、清晰表达意图、灵活性和易于维护扩展。最后,文章鼓励读者深入探索和应用函数式编程,并提供了加入知识星球的方式。

关键观点总结

关键观点1: 函数式编程在Java开发中的应用

文章介绍了如何使用Java 8的函数式接口来重构数据有效性断言逻辑,展示了函数式编程在减少重复代码和提高代码质量方面的优势。

关键观点2: ensureColumnValueValid方法的使用

文章介绍了一种通用的数据有效性断言方法ensureColumnValueValid,该方法接受待验证的值、实体类属性提取函数、单条数据查询执行器等参数,能够进行针对特定属性的有效性检查。

关键观点3: validateColumnValueMatchesExpected和validateColumnValueInExpectedList方法的应用场景

文章介绍了两个扩展方法validateColumnValueMatchesExpected和validateColumnValueInExpectedList,分别用于验证查询结果中的指定列值是否匹配预期值和是否位于预期值列表内。

关键观点4: 函数式编程的优势

文章讨论了函数式编程的优势,包括代码复用、清晰表达意图、灵活性和易于维护扩展等。

关键观点5: 鼓励深入探索和应用函数式编程

文章鼓励读者深入探索和应用函数式编程,并提供了加入知识星球的方式,以获取更多技术知识和实践。


正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本

来源:juejin.cn/post/
7384256110280572980


前言

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 的魔法棒:函数式接口

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 == nullreturn;
    
    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());     
    
    // 创建订单的逻辑...
}

对比上述两段代码,我们发现后者不仅大幅减少了代码量,而且通过函数式编程,表达出更为清晰的逻辑意图,可读性和可维护性都有所提高。

优点
  1. 减少重复代码: 通过 ensureColumnValueValid 方法,所有涉及数据库字段值有效性检查的地方都可以复用相同的逻辑,将变化的部分作为参数传递,大大减少了因特定校验逻辑而产生的代码量。
  2. 增强代码复用: 抽象化的校验方法适用于多种场景,无论是用户ID、订单号还是其他任何实体属性的校验,一套逻辑即可应对。
  3. 提升可读性和维护性: 通过清晰的函数签名和 Lambda 表达式,代码意图一目了然,降低了后续维护的成本。
  4. 灵活性和扩展性: 当校验规则发生变化时,只需要调整 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 == nullreturn;

   // 获取查询结果中目标列的实际值
   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






请到「今天看啥」查看全文