以若依为例讲解函数式接口的应用

897 阅读6分钟

前言

函数式接口作为Java8的新特性之一,和Stream一样可以让Java代码变得更加简洁优雅,而本文将以若依项目的代码为例,讲解如何使用函数式接口重写其中关于角色管理、岗位管理、参数管理等的新增和修改校验的代码,提供一个在日常中使用函数式编程优化代码的思路,如有错误之处,欢迎一起讨论。

逻辑梳理

在正式改造之前,我们先梳理一下新增和修改中的校验逻辑,以参数管理为例,在新增或者修改时,会对参数的键名进行校验:

image-20210910153131723

在新增的时候,整个校验逻辑则比较简单,只需要按照参数键名到数据库到数据库中进行查询即可,如果查询到结果则直接提示参数键名已存在,不允许新增即可,如果未查询到结果,则可以进行新增。

如果是需要对一条已存在的参数记录进行修改,对键名的校验逻辑就变成了在查询的时候需要排除当前修改的参数记录,然后在剩下的记录中查询参数键名是否存在,如果使用纯SQL语句查询的话则需要使用!=,而若依项目中则没有直接使用!=,而是直接根据参数键名到数据库中查询一条记录(未查到记录则一定可以进行修改),然后在代码层面对查询到的参数记录的id和当前修改的参数记录的id进行比较,如果相同,则说明参数键名只有当前参数记录使用,可以进行修改。如果不同,则说明当前修改的键名已经被其他的参数记录使用,则不允许修改。避免了在数据量较大时的使用!=的效率问题。

而在若依项目中,则进一步对新增和修改中的参数键名校验进行了简化合并,用一个变量存储当前新增或修改记录的id,如果是新增由于不会存在id,则赋值为-1(正常的记录id不会为-1),这样就可以保证新增和修改的唯一性判断一致。如果根据键名未查询到记录,则说明键名可以使用,如果查询到记录,修改的逻辑仍同上,则-1由于一定和查询到的记录id不同,说明参数键名已被使用,这样就让新增和修改的唯一性校验可以使用统一的方法。

附若依项目的参数键名校验代码如下:

public String checkConfigKeyUnique(SysConfig config) {
    // 获取当前新增或修改记录的 id, 新增记录由于传递的时候存在 id, 始终为-1
    Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId();
    // 根据键名查询参数记录
    SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey());
    // 如果查询到记录, 并且查询到记录的 id 与当前新增或修改记录的 id 相同, 则说明执行参数键名无法使用
    if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) {
        return UserConstants.NOT_UNIQUE;
    }
    // 如果不存在指定参数键名的记录, 则说明可以使用
    return UserConstants.UNIQUE;
}

业务提取

就像参数管理中对参数键名一样,角色管理、岗位管理等中也都存在都某个字段的唯一性校验。而这些唯一性校验除了校验时传递的参数和底层查询的SQL表不同,其它则都是统一的逻辑:

  1. 获取当前新增或修改记录的 id.
  2. 根据字段条件查询记录.
  3. 对查询到的结果是否为空及id是否相同做唯一性校验并返回结果。

使用函数式接口进行改造

首先先展示根据业务提取出来的方法,为了便于理解,整体代码逻辑依然参考若依的代码逻辑:

public static <E> String checkFieldUnique(E entity, Function<E, Long> getIdFun, 
                                          UnaryOperator<E> checkFieldUniqueFun) {
    Long id = StringUtils.isNull(getIdFun.apply(entity)) ? -1L : getIdFun.apply(entity);
    E e = checkFieldUniqueFun.apply(entity);
    if (StringUtils.isNotNull(e) && getIdFun.apply(e).longValue() != id.longValue())
    {
        return UserConstants.NOT_UNIQUE;
    }
    return UserConstants.UNIQUE;
}

下面再来介绍这个方法中的三个参数:

  1. E entity

    这个比较好理解,通过使用泛型,便于处理不同的实体类型。

  2. Function<E, Long> getIdFun

    这个可以根据参数名知道这是一个获取实体id的函数,再结合前面的类型可以大致猜出这是一个根据实体(E)获取id(Long)的函数,这样在调用该方法的时候,如果是参数管理,则传递SysConfig::getConfigId即可,这里可以看做将SysConfig类的getConfigId方法引用传递给了checkFieldUnique,这样就可以在方法内容按需调用该方法,可以看作是js中将一个函数作为参数传递给另一个函数,类似如下代码:

    // 这里用了一个刻意的例子, 仅为了便于理解 Java 中的方法引用
    const getNumArr = () => [1, 2, 3, 4, 5, 6, 7]
    const getStrArr = () => ['A', 'B', 'C', 'D']
    ​
    const printArr = getArrFun => {
        const arr = getArrFun()
        for (let item of arr) {
            process.stdout.write(`${item} `)
        }
        console.log()
    }
    ​
    printArr(getNumArr)  // 1 2 3 4 5 6 7
    printArr(getStrArr)  // A B C D
    

    也可以看作是c语言的函数指针:

    #include <stdio.h>int sum(int a, int b) 
    {
        printf("%d + %d = %d", a, b, a + b);
    }
    ​
    void demo(int(*fun)(int, int), int a, int b) {
        fun(a, b);
    }
    ​
    int main()
    {
       
       demo(sum, 1, 2);  // 1 + 2 = 3
       
       return 0;
    }
    
  3. UnaryOperator<E> checkFieldUniqueFun

    从参数名可以看出这是一个校验字段唯一的函数, UnaryOperator<E>则代表这是一个参数为E(实体类型)并且返回值也为E(实体类型)的函数,不过在上面介绍参数键名校验的时候,可以看到校验字段唯一性的函数参数只有一个String类型的字段,但是这里却需要传递整个实体类了,之所以进行这个改造,则是为了便于某些情况下可能需要根据实体的多个字段进行字段的唯一性校验,例如若依中的部门管理就需要传递两个字段:

    /**
     * 校验部门名称是否唯一
     * 
     * @param deptName 部门名称
     * @param parentId 父部门ID
     * @return 结果
     */
    public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId);
    

实际改造

下面就对部门管理的代码进行改造来展示最终的效果,首先改造校验字段唯一性的参数为实体类型:

/**
 * 校验部门名称是否唯一
 * 
 * @param dept 部门信息
 * @return 结果
 */
public SysDept checkDeptNameUnique(SysDept dept);

然后修改校验部门名称的方法内部代码,这里的checkFieldUnique方法即为上文中提取的函数式接口方法:

public String checkDeptNameUnique(SysDept dept) {
    return checkFieldUnique(dept, SysDept::getDeptId, deptMapper::checkDeptNameUnique);
}

然后进行测试:

image-20210911091253216

可以发现代码正常运行,这样以后如果在新增或者修改某个实体时,如果需要对实体的某个字段进行唯一性的校验,只需要统一调用checkFieldUnique即可,而不再需要重复的书写相同校验逻辑的代码。

总结

本文通过一个简单的改造若依的唯一性校验的代码讲解了函数式编程在实际开放中的实际应用,其实很多时候,我们都可以发现代码中存在很多整体逻辑相同,但是部门方法调用不同的代码,这时就可以考虑使用函数式接口方法进行提取,简化代码。