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;
}
将两段代码进行对比:
得出结论:
- 结构相似:外形
- 语义相似:分页语义一致,先查询 count,然后再根据 count 是否查询 List 详情
如果再有其他实体对象的分页,那么 CV 一下,改改我上面的红框的地方即可。
基于上面这个案例,我们再深度思考一下。
2.2 重复代码的考量
在系统里面,这样结构化的代码随处可见。那么这两个方法代码有重复代码吗?
好像并没有(IDEA没有提示),因为很难找到大块重复的代码;也很难抽取出来一个具体的方法,然后被调用!
就像下面图中展示的那样,选中重复代码,然后抽取一个新的方法,对重复代码进行替换!下面的这种操作就是我早期进行代码重构的核心技能!
虽然很难找到大块重复代码,但是上面的代码从 “外形骨架”
上看,却极其相似,难道这种相似不应该也是一种重复吗?
这种结构化式的重复,曾经困扰了我很久,我很难像抽取重复代码一样去抽取这种相似的结构!
遇到这样结构化的代码,我也不得不加入 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,则创建一个空的集合对象,否则返回原来的集合对象
泛型为简化重复代码而生!
泛型的适用的场景太多,比如下面场景:
- 工具类中使用
- 抽象类;模板方法,构建标准步骤中使用
- 顶层接口类中使用
- 甚至使用 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 了。泛型和函数式的编程让我的代码重复率又下降一个水位!
小秘诀:将变动的部分以函数方式进行变量替换;从而保留骨架,达到泛化和通用。就像下面这张图一样,“骨肉分离”,肉是细节代码;骨是结构框架。
📚更多案例
我开始大量实践后,这样的代码也越来越多。接着再分析一个详细的案例。
下面是一个异步回调的工具类。
首先,定义异步回调的任务接口,所有目标对象需要实现该接口才能作为异步回调的参数进行调用
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 中的语法糖,它算不上编程的内功心法,只是一种展现形式而已。我们更多应该关注的是如何对一系列具体的场景进行抽象,然后再通过工具去实现它们。就像如何去定义一个泛型,如何去抽象一个函数一样。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!