写代码这件事,迈入第七个年头才有了一些心得(第四章 泛型 + 函数式编程)

6,874 阅读9分钟

写代码这件事,迈入第七个年头才有了一些心得(第四章 泛型 + 函数式编程)

将泛型和函数式编程结合,只是为了让代码更加优雅! --uzong

🍁一、成长往事

CV大法是入门时候的必备技能,但随着对自己技术要求的提高,对于重复的代码也开始变得抵触。 于是我开始抽取那些看得见的重复代码;有一天突然发现,虽然我抽取了大量重复性的代码;但太多 "骨架相似"、"结构化式" 的代码却若隐若现,虽然代码上它们不重复,但从"外形"却极其相似!,就像下图一样,看起来不相同,其形状却相似。

image.png

我以为把重复代码抽成一个独立的方法,从而实现代码的复用,这就是所谓的重构优化;但这种方式却太表象了,没有灵魂和深度,过去的那些日子,我感觉自己的编程水平也就限于把重复的代码抽一抽,(如下图所示一样),甚至觉得代码优化不就是这样吗,这样的状态一直维持很久。

image.png

随着对代码本身的思考,我才慢慢体会到了编程的艺术,也才慢慢体会到了代码其实也可以更优雅! 而让我感受到这种优雅艺术的点,正是泛型和函数式编程!

今天就带大家一步一步地感受,泛型和函数式编程的优雅所在!

📒二、案例分析

2.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);
    
    // 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;
}

将两段代码进行对比:

bookpencil
image.pngimage.png

得出结论:

  • 结构相似:外形
  • 语义相似:分页语义一致,先查询 count,然后再根据 count 是否查询 List 详情

如果再有其他实体对象的分页,那么 CV 一下,改改我上面的红框的地方即可。

基于上面这个案例,我们再深度思考一下。

2.2 重复代码的考量

在系统里面,这样结构化的代码随处可见。那么这两个方法代码有重复代码吗?

好像并没有(IDEA没有提示),因为很难找到大块重复的代码;也很难抽取出来一个具体的方法,然后被调用!

就像下面图中展示的那样,选中重复代码,然后抽取一个新的方法,对重复代码进行替换! 下面的这种操作就是我早期进行代码重构的核心技能!

image.png

虽然很难找到大块重复代码,但是上面的代码从 “外形骨架” 上看,却极其相似,难道这种相似不应该也是一种重复吗?

这种结构化式的重复,曾经困扰了我很久,我很难像抽取重复代码一样去抽取这种相似的结构!

遇到这样结构化的代码,我也不得不加入 CV 大军;并自我PUA,这样的代码并不是重复的代码!!!

随着对代码的思考和深入,一种独特的组合,彻底解决这种结构化的重复,那便是泛型+函数式编程

📑三、泛型 + 函数式编程

我认为它们的组合是天生解决这种结构化的!

3.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,则创建一个空的集合对象,否则返回原来的集合对象

image.png

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

泛型的适用的场景太多,比如下面场景:

  • 工具类中使用
  • 抽象类;模板方法,构建标准步骤中使用
  • 顶层接口类中使用
  • 甚至使用 Object 的场景都可以考虑使用泛化来替代

......

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

3.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 了。泛型和函数式的编程让我的代码重复率又下降一个水位!

小秘诀:将变动的部分以函数方式进行变量替换;从而保留骨架,达到泛化和通用。就像下面这张图一样,“骨肉分离”,肉是细节代码;骨是结构框架。

image.png

📚五、更多案例

我开始大量实践后,这样的代码也越来越多。接着再分析一个详细的案例。

下面是一个异步回调的工具类。

首先,定义异步回调的任务接口,所有目标对象需要实现该接口才能作为异步回调的参数进行调用

public interface CallbackTask<R> {
    R execute();
    default void onSuccess(R r) {
    }
    default void onFailure(Throwable t) {
    }
}

方法理解:

  • execute 主方法,目标任务需要具体实现
  • 方法执行成功后回调 onSuccess 方法
  • 方法执行失败后回调 onFailure 方法

下面是具体实现:通过 CompletableFuture 来实现异步回调!调用执行链路逻辑:

  • supplyAsync 异步执行任务
  • whenComplete 异步执行结束回调,不管失败成功都会调用,因此我做了一下判断
  • exceptionally 失败场景回调
/**
 * 借助 CompletableFuture 来实现异步行为。
 * 不会抛出异常,在 onFailure 中处理异常
 *
 * @param executeTask
 * @param <R>
 * @return
 */
private static <R> CompletableFuture<R> doInvoker(
            CallbackTask<R> executeTask) {
    CompletableFuture<R> invoke = CompletableFuture
            .supplyAsync(() -> {
                try {
                    return executeTask.execute();
                } catch (Exception exception) {
                    throw new BizException(
                        ASYNC_INVOKER_ERROR.getErrorCode(), 
                        exception.getMessage());
                }
            }, gPool)
            .whenComplete((result, throwable) -> {
                // 不管成功与失败,whenComplete 都会执行,
                // 通过 throwable == null 跳过执行
                if (throwable == null) {
                    executeTask.onSuccess(result);
                }
            })
            .exceptionally(throwable -> {
                executeTask.onFailure(throwable);
                // todo 给一个默认值,或者使用 Optional包装一下,否者异常会出现NPE
                return null;
            });
    return invoke;
}

上面代码是整个骨架, 实现了异步回调。

下面是具体使用:

CompletableFuture<Integer> result = 
    AsyncInvoke.doInvoker(new CallbackTask<Integer>() {      
        public Integer execute()  {
            int result = 1 + 1;
            return result;
        }

        @Override
        public void onSuccess(Integer integer) {
            System.out.println("on success result: " + integer);
        }

        @Override
        public void onFailure(Throwable t) {
            System.out.println("error " + t.getMessage());
        }
});
  • result#get 可以获取异步结果
  • 执行成功后调用 onSuccess,失败会调用 onFailure

还有很多其他场景都可以使用 泛型 + 函数式编程 来解决

  • 针对每个方法限流
  • 针对每个方法重试

......

它解决了重复,让代码看起来优雅!

虽然如此,但这样的组合,也会带来一些不足。

📝六、不足之处

  • 泛型和函数式编程不方便调试;出问题的时候比较显著
  • 理解有成本,尤其方法不熟悉的时候
  • 同时降低一定的可读性

🔊七、最后感想

泛型和函数式编程只是 Java 中的语法糖,它算不上编程的内功心法,只是一种展现形式而已。我们更多应该关注的是如何对一系列具体的场景进行抽象,然后再通过工具去实现它们。就像如何去定义一个泛型,如何去抽象一个函数一样。

🍁🍁🍁 重复等于不精彩,我不喜欢!