引例——抽取工具类
先来看看这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| @GetMapping(path = "/getUserInfo") public UserInfoDTO getUserInfo(@RequestParam("userId") String userId) { logger.info("userId: " + userId + " visit path: /getUserInfo");
try { if (userId == null || !userId.startsWith("u")) { throw new RuntimeException("param userId is invalid"); }
UserBaseInfoVO userBaseInfoVO = userBaseInfoRepository.getUserBaseInfo(userId);
UserSpecialInfoVO userSpecialInfoVO = userSpecialInfoRepository.getUserSpecialInfo(userId);
UserMoneyVO userMoneyVO = UserMoneyRepository.getUserMoney(userId);
List<ConsumeRecordVO> consumeRecordVOList = userConsumeRepository.getConsumeRecordVOList(userId);
Optional<ConsumeRecordVO> max = consumeRecordVOList.stream() .max(Comparator.comparing(ConsumeRecordVO::getAmount)); ConsumeDTO maxAmountConsume = null; if (max.isPresent()) { ConsumeRecordVO consumeRecordVO = max.get(); maxAmountConsume = ConsumeDTO.builder() .amount(consumeRecordVO.getAmount()) .date(consumeRecordVO.getDate()) .build(); }
DoubleSummaryStatistics total = consumeRecordVOList.stream() .collect(Collectors.summarizingDouble(ConsumeRecordVO::getAmount)); Double totalMoney = userMoneyVO.getMoney() + total.getSum();
logger.info("userId: " + userId + " visit path: /getUserInfo success");
return UserInfoDTO.builder() .userName(userBaseInfoVO.getUserName()) .vipLevel(userBaseInfoVO.getVipLevel()) .maxAmountConsume(maxAmountConsume) .totalMoney(totalMoney) .build(); } catch (Exception e) { logger.error("userId: " + userId + " visit path: /getUserInfo fail. exception message = " + Arrays.toString(e.getStackTrace())); return null; } }
|
看上去不错,对吧
但是当我们有两个这种接口
graph TD
subgraph getOtherInfo
A1[打印入参]
A3[校验入参与执行业务逻辑]
A4[打印出参]
A5[处理异常]
A6[返回结果]
A1 --> A3
A3 --> A4
A4 --> A6
A3 -->|校验失败或业务逻辑异常| A5
A5 --> A6
end
subgraph getUserInfo
B1[打印入参]
B3[校验入参与执行业务逻辑]
B4[打印出参]
B5[处理异常]
B6[返回结果]
B1 --> B3
B3 --> B4
B4 --> B6
B3 -->|校验失败或业务逻辑异常| B5
B5 --> B6
end
是不是看出来问题了?
问题:相同能力没有复用
我们会发现他们逻辑不一致的地方只有校验入参和业务逻辑,其他相似或相同逻辑的地方没有复用
- 如果新增接口,所有的日志打印要冗余写一遍,包括入口日志、出口日志、异常日志
- 如果新增接口,try-catch的异常处理逻辑也需要冗余重写
- 如果新增一个只获取用户金额信息的接口,需要冗余复制上述代码中和金额相关的部分
- 如果接口需要修改,返回新的信息,那就需要往这个代码里添加新的业务逻辑。而这个类一旦有变化,就涉及对这个类的回归验证
对于这种情况,我们可能会给他抽出一个工具类
graph TD
subgraph 接口A
打印入参 --> A校验入参和执行业务逻辑
A校验入参和执行业务逻辑 -->|校验失败或业务异常| 处理异常
A校验入参和执行业务逻辑 --> 打印出参
处理异常 --> 返回结果
end
subgraph 接口B
打印入参 --> B校验入参和执行业务逻辑
B校验入参和执行业务逻辑 -->|校验失败或业务异常| 处理异常
B校验入参和执行业务逻辑 --> 打印出参
打印出参 --> 返回结果
end
紧接着带来的问题
- 你怎么保证别人以后一定用工具类?
- 你怎么保证别人以后一定正确使用工具类?(如顺序有误)
- 用的人怎么确定需要用哪些工具类?(线上系统共同逻辑很多,难以定位需要的工具类)
正解——模板方法
抽象一个模板,规定顺序是打印入参、校验入参、业务逻辑、打印出参。
- 打印入参和打印出参有默认实现
- 校验入参、业务逻辑是抽象的,接口A、接口B各自实现
- 统一异常处理
graph LR
subgraph 模板
打印入参 --> 校验入参
校验入参 --> 业务逻辑
业务逻辑 --> 打印出参
打印出参 --> 结束
end
subgraph 接口A
校验入参A -->|实现| 校验入参
业务逻辑A -->|实现| 业务逻辑
end
subgraph 接口B
校验入参B -->|实现| 校验入参
业务逻辑B -->|实现| 业务逻辑
end
特点:统一逻辑,标准化流程
从系统的设计上
- 每个接口都不用担心忘了执行必要的公共逻辑,例如打印日志、异常处理。
- 不用担心接口有遗漏步骤及搞错步骤顺序,例如入参校验在执行业务流程之前。
- 接口只需要关心自己业务逻辑的实现即可。
- 所有接口打印的日志及异常处理方式确保是一致的,方便监控和定位问题。
- 如果需要增加一些公用的能力,例如埋点上报某个统计平台,只需要在框架中添加逻辑,所有接口都直接生效。
简单来说,就是统一逻辑,标准化流程
模板代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public abstract class ServiceTemplate<T, R> { private Logger logger = new LoggerImpl();
public R process(T request) { logger.info("start invoke, request - " + request);
Stopwatch stopwatch = Stopwatch.createStarted(); try { validParam(request);
R response = doProcess(request);
Long timeCost = stopwatch.elapsed(TimeUnit.MILLISECONDS); logger.info("end invoke, response = " + response + ", costTime = " + timeCost); return response; } catch (Exception e) { logger.error("error invoke, exception: " + Arrays.toString(e.getStackTrace())); return null; } }
protected abstract void validParam(T request);
protected abstract R doProcess(T request); }
|
服务实现示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping(path = "doSomething") public Response doSomething(Request request) { return (new ServiceTemplate<RequestResponse>() { @Override public void validParam(Request request) { }
@Override public Response doProcess(Request request) { return new Response(); } }).process(request); }
|
服务实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public UserInfoDTO getUserInfo(@RequestParam("userId") String userId) { return (new ServiceTemplate<String>() { @Override public void validParam(String request) { if (userId == null || userId.isEmpty()) { throw new RuntimeException("param UserId is invalid"); } }
@Override public UserInfoDTO doProcess(String request) { UserBaseInfoVO userBaseInfoVO = userBaseInfoRepository.getUserBaseInfo(userId);
UserSpecialInfoVO userSpecialInfoVO = UserSpecialInfoRepository.getSpecialInfoVO(userId);
UserMoneyVO userMoneyVO = userMoneyRepository.getUserMoneyVO(userId);
List<ConsumeRecordVO> consumeRecordVOList = userConsumeRepository.getConsumeRecordVOList(userId);
Optional<ConsumeRecordVO> max = consumeRecordVOList.stream() .max(Comparator.comparing(ConsumeRecordVO::getAmount)); ConsumeDTO maxAmountConsume = null; if (max.isPresent()) { ConsumeRecordVO consumeRecordVO = max.get(); maxAmountConsume = ConsumeDTO.builder() .amount(consumeRecordVO.getAmount()) .date(consumeRecordVO.getDate()) .build(); }
DoubleSummaryStatistics total = consumeRecordVOList.stream() .collect(Collectors.summarizingDouble(ConsumeRecordVO::getAmount)); double totalMoney = userMoneyVO.getMoney() + total.getSum();
return UserInfoDTO.builder() .userName(userBaseInfoVO.getUserName()) .vipLevel(userBaseInfoVO.getVipLevel()) .maxAmountConsume(maxAmountConsume) .totalMoney(totalMoney) .build(); } }).process(userId); }
|
问题
即使这样,系统还不够完美,当我们:
- 加逻辑:
getUserlnfo
新增加用户优惠券信息
- 改逻辑:最贵一次消费记录要改成近一个月的
- 增加复杂判断:如果是授权了的用户,才查询余额,没有授权不能查
我们的代码都在一个文件里加,会导致代码:
- 腕肿难懂:看起来费劲,而且不同同学代码风格不一致,导致注释还可能难懂和有歧义
- 容易相互影响:太多行,一不小心改错了;又或者有依赖顺序、数据结构不能有变动
- 不好测试:为了测方法中的某一处逻辑,不得不在方法入参上引入大量无关数据进行mock。而且万一中途某一块其他逻辑判断非常复杂,那mock难度就是指数级上升
那么应该怎么解决呢?详情请看下节
参考资料
[^1]: 【学架构也可以很有趣】【“趣”学架构】- 2.搭系统得先搭架子
[^2]: 【成为架构师】8. 成为工程师 - 搭建系统先搭建框架