• 如何使用泛型和函数式编程让代码更优雅?
  • 发布于 2个月前
  • 258 热度
    1 评论

一.案例分析

1.1. 结构化的代码

以分页为例子,来感受一下什么是结构化的代码。特别说明一下:
分页还需当前页数、页大小,以及校验等,本案例忽略;
代码主要逻辑:查询分页条数,如果为 0 ,则不用查询列表详情,直接返回;如果分页条数大于 0 则查询列表详情。
代码一、返回总数和分页详情,查询 Book 表:
public PageData queryBook(BookRequest request) {
     // 1. 创建分页对象
    PageData pageData = new PageData();

    // 2. 计算满足的记录数
    int count = bookMapper.queryBookCount(request); 
  
    // 3. 为 0,则表示没有符合的数据,直接返回
    if (count == 0) {
        pageData.setCount(0);
        return pageData;
    }    
    // 4. 不为 0,计算记录详情
    List<BookDO> bookList = bookMapper.queryBookList(request);
    // 堆代码 duidaima.com
    // 5. 封装记录总数和
    pageData.setCount(count);
    pageData.setResult(bookList);
    
    return pageData;
}
代码二、返回总数和分页详情,查询 Pencil 表:
public PageData queryPencil(PencilRequest request) {
     // 1. 创建分页对象
    PageData pageData = new PageData();

    // 2. 计算满足的记录数
    int count = pencilMapper.queryPencilCount(request); 
  
    // 3. 为 0,则表示没有符合的数据,直接返回
    if (count == 0) {
        pageData.setCount(0);
        return pageData;
    }    
    // 4. 不为 0,计算记录详情
    List<PencilDO> pencilList = pencilMapper.queryPencilList(request);
    
    // 5. 封装记录总数和
    pageData.setCount(count);
    pageData.setResult(pencilList);
    
    return pageData;
}
将两段代码进行对比:

得出结论:
1.结构相似:外形
2.语义相似:分页语义一致,先查询 count,然后再根据 count 是否查询 List 详情
3.如果再有其他实体对象的分页,那么 CV 一下,改改我上面的红框的地方即可。
基于上面这个案例,我们再深度思考一下。

1.2. 重复代码的考量
在系统里面,这样结构化的代码随处可见。那么这两个方法代码有重复代码吗?好像并没有(IDEA没有提示),因为很难找到大块重复的代码;也很难抽取出来一个具体的方法,然后被调用!就像下面图中展示的那样,选中重复代码,然后抽取一个新的方法,对重复代码进行替换!下面的这种操作就是我早期进行代码重构的核心技能!

虽然很难找到大块重复代码,但是上面的代码从 “外形骨架” 上看,却极其相似,难道这种相似不应该也是一种重复吗?这种结构化式的重复,曾经困扰了我很久,我很难像抽取重复代码一样去抽取这种相似的结构!遇到这样结构化的代码,我也不得不加入 CV 大军;并自我PUA,这样的代码并不是重复的代码!随着对代码的思考和深入,一种独特的组合,彻底解决这种结构化的重复,那便是泛型+函数式编程。

二.泛型 + 函数式编程
我认为它们的组合是天生解决这种结构化的!
2.1 泛型特性
泛型,'一切皆行',泛型在于它的普适性、通用性。太多工具类都使用了泛型!当然我也喜欢用泛型,因为它很优雅!下面是一个代码片段。对请求返回结果进行包装;特别适用于 RPC 远程调用结果等场景。针对不同的返回实体,可以使用 T 类型 来表示。非常通用!
T 代表一切实体
success 是否请求成功
errorCode、errorMsg 出错时提供错误码和错误消息
public class ServiceResult<T> {
    
    /**
     * 请求是否成
     */ 
    private Boolean success;
    
    /**
     * 错误码
     */
    private String errorCode ;

    /**
     * 错误信息
     */
    private String errorMsg;
    
    /**
     * 返回内容
     */
    private T content;
    
    ......
    
}
除了上面这个案例以外,在很多工具类库中都十分常见。例如:下面是 hutool 包中的一个工具类。一个 set 集合如果为 null,则创建一个空的集合对象,否则返回原来的集合对象

泛型为简化重复代码而生!

泛型的适用的场景太多,比如下面场景:
工具类中使用
抽象类;模板方法,构建标准步骤中使用
顶层接口类中使用
甚至使用 Object 的场景都可以考虑使用泛化来替代
......

接下来再聊聊从 JDK8 开始的新特性(语法糖),函数式编程。

2.2 函数式编程
在函数式编程中,可将方法作为参数进行传递调用;灵活性不言而喻!下面这几行代码是基于 guava 的 ListenableFuture 封装的一个异步回调。Callable 可以代表所有的方法。(匿名内部类)
public static <V> ListenableFuture<V> invokeWithFuture(
       Callable<V> callable) {
    return gPool.submit(callable);
}
使用如下:
@Test
public void invokeWithFuture() throws Execution {
    ListenableFuture<String> result = 
              AsyncInvoke.invokeWithFuture(() -> 'hello');

    System.out.println(result.get());
}
() -> 'hello' 作为一个代码块被传入到了方法中。是的,将代码块作为参数传递!函数式编程的好处,让代码变得如此的灵活。困扰我多年的问题终于有了解法了。

华丽转身
泛型:解决通用性
函数式编程:将代码块用函数作为参数进行传递
于是基于分页的结构化问题,使用 泛型 + 函数式编程 进行解决!
分页总数,使用 countFunction 计算
分页详情,使用 listFunction 获取
如下所示:
@SneakyThrows
public static <T> PageData buildPageData(
            Callable<Integer> countFunction, 
            Callable<List<T>> listFunction) {
    // 1. 创建分页对象
    PageData pageData = new PageData();

    // 2. 计算满足的记录数
    int count = countFunction.call();

    // 3. 为 0,则表示没有符合的数据,直接返回
    if (count == 0) {
        pageData.setCount(0);
        return pageData;
    }
    // 4. 不为 0,计算记录详情
    List<T> resultList = listFunction.call();


    // 5. 封装记录总数和
    pageData.setCount(count);
    pageData.setResult(resultList);

    return pageData;
}
补充分页对象代码:
@Data
static class PageData<T> {
    private int count;
    private T result;
}
使用情况:
@Test
public void testBuildPageData(String[] args) {
    buildPageData(()-> 1, () -> Arrays.asList('1'));
}
第一个参数是求记录数的方法
第二个参数是求详情的方法
从那时起,结构化的代码,我不再进行 CV 了。泛型和函数式的编程让我的代码重复率又下降一个水位!

用户评论