代码精简之路-模板模式
代码精简之路-模板模式
1. 前言
程序员怕重复CRUD,总是做一些简单繁琐的事情。“不要重复造轮子”,“把基础功能提炼出来封装成工具类” 我喜欢把这些话挂在嘴边,写起来常不知从何下手。 下面拆解一个项目中的功能。记录从复制粘贴到对业务抽象、实现功能分层的详细过程。如何着手提升代码重构优化能力,拿到项目需求用自己的思维实现一遍,再到维护发现其中的不足,再模仿优化。自己踩坑发现问题再自己解决是最有效的方式。
2. 需求
XX申报系统,接受用户申报数据,系统对申报数据做格式检查,再对单证中的一些字段(如状态、单证号、创建时间等)赋初始值,再保存入库。单证类型有:订单、运单、支付单、清单、申报单。
3. 原始代码的不足
a. 流水代码,比如数据格式检查中大量用到if else的判断。
b. 时间等格式检查代码在不同单证中重复出现。(因为用户上传的excel申报数据中时间格式多样,甚至有中文年月字样,时间字段才用的字符串类型。)
c. 结构混乱,数据校验、赋初始值、保存等功能交叉在一起。
原订单处理代码:
@Service @RequiredArgsConstructor public class OrderHandler {
private final CurrencyService currencyService;
private final EbcService ebcService;
private final OrderDao orderDao;
public Result start(Order order) {
Result result \= new Result();
result.setSuccess(true);
if (Strings.isNullOrEmpty(order.getAgentName())) {
result.setSuccess(false);
result.getErrors().add("代理人为空");
}
if (Strings.isNullOrEmpty(order.getCurrency())) {
result.setSuccess(false);
result.getErrors().add("币制编码为空");
} else {
// 赋初始值混在数据检查中
order.setCurrencyName(currencyService.getName(order.getCurrency()));
}
if (Strings.isNullOrEmpty(order.getEbcCode())) {
result.setSuccess(false);
result.getErrors().add("电商为空");
} else {
// 赋初始值混在数据检查中
order.setEbcName(ebcService.getName(order.getEbcCode()));
}
if (Strings.isNullOrEmpty(order.getConsignee())) {
result.setSuccess(false);
result.getErrors().add("收货人为空");
}
if (Strings.isNullOrEmpty(order.getConsigneeTelephone())) {
result.setSuccess(false);
result.getErrors().add("收货人电话为空");
}
if (Strings.isNullOrEmpty(order.getOrderDate())) {
result.setSuccess(false);
result.getErrors().add("订单时间为空");
} else {
// 检查时间格式
boolean timeValid = false;
try {
DateTimeFormatter formatter \= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalTime.parse(order.getOrderDate(), formatter);
timeValid \= true;
} catch (DateTimeParseException e) {
e.printStackTrace();
}
// 多种时间格式
if (!timeValid) {
try {
DateTimeFormatter formatter \= DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
LocalTime.parse(order.getOrderDate(), formatter);
timeValid \= true;
} catch (DateTimeParseException e) {
e.printStackTrace();
}
}
if (!timeValid) {
result.setSuccess(false);
result.getErrors().add("订单时间格式错误");
}
}
// todo: 其它格式检查代码
if (result.isSuccess()) {
order.setStatus(OrderStatus.APPLY.getCode());
order.setCreateTime(LocalDateTime.now());
orderDao.save(order);
}
return result;
}
}
View Code
4. 改进一一初步封装
a. 实现对数据检查的功能封装,将基础功能与业务解耦。这里的基础功能是指格式检查,业务是指对不单证中的字段值赋初始值和保存。解耦的好处:有利于将来在别的项目或功能模块中复用基础功能。同时本系统中业务功能调整也不需要改动基础功能部分的代码。
改进后的订单处理代码:
public Result start(Order order) { Result result = new Result(); result.setSuccess(true); // todo:其它格式检查 // 封装时间格式检查功能 boolean timeValid = validator.checkTime(order.getOrderDate()); if (!timeValid) { result.setSuccess(false); result.getErrors().add("订单时间格式错误"); } // todo:赋初始值 // todo:保存 return result; }
View Code
格式检查代码封装,实现多种格式的时间检查:
public boolean checkTime(String time) {
List
View Code
5. 改进一一模板模式
a. 实现业务抽象,建立对检验、初始化、保存的标准流程。
b. 实现代码分层,抽象基类负责定义标准流程,实现类负责各业务功能具体实现。上层(抽象基类)负责制定标准,下层负责执行标准。
基类代码:
/**
* 抽象基类,定义处理流程
*
* @param
/\*\*
\* 接收申报处理入口:校验、赋初值、保存都在这里实现了,下层类不需要写流程处理的重复代码
\*
\* @param doc
\* @return
\*/
public Result start(T doc) {
if (doc == null) {
return new Result(false);
}
// step1
Result result = check(doc);
// 如果数据校验不通过直接返回
if (!result.isSuccess()) {
return result;
}
try {
// step2,3
init(doc);
save(doc);
result.setSuccess(true);
} catch (Exception e) {
e.printStackTrace();
result.setSuccess(false);
result.getErrors().add(e.getMessage());
}
return result;
}
/\*\*
\* 数据校验方法,业务类分别实现
\*
\* @param doc
\* @return
\*/
protected abstract Result check(T doc);
/\*\*
\* 数据初始化
\*
\* @param doc
\*/
protected abstract void init(T doc);
/\*\*
\* 保存
\*
\* @param doc
\*/
protected abstract void save(T doc);
}
View Code
改进订单处理代码,只需填充基类模板空出来的3个方法:
@Component
@RequiredArgsConstructor
public class OrderHandlerV3 extends BaseHandler
private final CurrencyService currencyService;
private final EbcService ebcService;
private final OrderDao orderDao;
private final Validator validator;
@Override
protected Result check(Order doc) {
Result result \= new Result();
result.setSuccess(true);
// 时间检查
boolean timeValid = validator.checkTime(doc.getOrderDate());
if (!timeValid) {
result.setSuccess(false);
result.getErrors().add("订单时间格式错误");
}
if (StringUtils.isBlank(doc.getAgentName())) {
result.setSuccess(false);
result.getErrors().add("代理人为空");
}
// todo:其它格式检查 ...
return result;
}
@Override
protected void init(Order doc) {
doc.setCurrencyName(currencyService.getName(doc.getCurrency()));
doc.setEbcName(ebcService.getName(doc.getEbcCode()));
doc.setStatus(OrderStatus.APPLY.getCode());
doc.setCreateTime(LocalDateTime.now());
// todo:其它字段初始化 ...
}
@Override
protected void save(Order doc) {
orderDao.save(doc);
}
}
View Code
改进运单处理代码,只需填充基类模板空出来的3个方法:
@Component
@RequiredArgsConstructor
public class WaybillHandlerV3 extends BaseHandler
private final CountryService countryService;
private final CurrencyService currencyService;
private final WaybillDao waybillDao;
private final Validator validator;
@Override
protected Result check(Waybill doc) {
Result result \= new Result();
result.setSuccess(true);
// 检查发货时间
boolean timeValid = validator.checkTime(doc.getDeliveryDate());
if (!timeValid) {
result.setSuccess(false);
result.getErrors().add("发货时间格式错误");
}
// todo:其它格式检查 ...
return result;
}
@Override
protected void init(Waybill doc) {
doc.setCurrencyName(currencyService.getName(doc.getCurrency()));
doc.setConsigneeCountryName(countryService.getName(doc.getConsigneeCountry()));
doc.setStatus(WaybillStatus.APPLY.getCode());
doc.setCreateTime(LocalDateTime.now());
// todo:其它字段初始化 ...
}
@Override
protected void save(Waybill doc) {
waybillDao.save(doc);
}
}
View Code
6. 优劣对比
a. 有利于阅读代码、维护功能。
- 原始代码中3个步骤(校验、赋初始值、保存)的功能在混合交叉在一起,在一个方法中实现,阅读维护非常耗时。将来如果需求变动,如字段长度变化/必填字段变化要修改数据检查部分代码;状态字段值变化(申报由1表示改为由A表示)而修改赋初始值部分代码;ORM框架变化修改dao的实例。这时就只能到这一个方法中寻找对应部分,要从头到尾阅读代码。
- 模板模式中实现对业务抽象、建立流程以后,代码结构层次清晰,只要需到抽象类或实现类的对应流程中去寻找修改。
b. 有利于功能升级。现在的功能只有3步,假如将来功能拓展,如对接别的系统平台(把合规的数据转为json格式推送给目标系统的接口)。
- 在原始代码中就需要分别到各个单证类中分别添加数据格式转换、推送接口的功能,再分别测试。
- 在模板模式代码中只需要求在基类的流程中再加两个步骤,甚至转换和推送都可以在基类中统一实现,相比之下编码和测试都减少了。
c. 功能升级举例,流程处理中增加推送功能:
- 格式转换和推送都在基类中完成。
- 各实现类中只需设置不同单证的推送接口。
升级后的基类,只增加了4行代码:
public abstract class BaseHandler
/\*\*
\* 接收申报处理入口:校验、赋初值、保存都在这里实现了,下层类不需要写流程处理的重复代码
\*
\* @param doc
\* @return
\*/
public Result start(T doc) {
if (doc == null) {
return new Result(false);
}
// step1
Result result = check(doc);
// 如果数据校验不通过直接返回
if (!result.isSuccess()) {
return result;
}
try {
// step2,3
init(doc);
save(doc);
result.setSuccess(true);
} catch (Exception e) {
e.printStackTrace();
result.setSuccess(false);
result.getErrors().add(e.getMessage());
}
// 升级功能,申报成功以后推送数据到别的平台
String json = JSONObject.toJSONString(doc);
boolean send = apiClient.send(json, getApi());
if (!send) {
// todo:记录推送失败日志
}
// todo: 记录推送记录等
return result;
}
/\*\*
\* 数据校验方法,业务类分别实现
\*
\* @param doc
\* @return
\*/
protected abstract Result check(T doc);
/\*\*
\* 数据初始化
\*
\* @param doc
\*/
protected abstract void init(T doc);
/\*\*
\* 保存
\*
\* @param doc
\*/
protected abstract void save(T doc);
/\*\*
\* 新增功能,获取推送接口
\*
\* @return
\*/
protected abstract String getApi();
}
View Code
升级后的订单处理类,只填充接口地址方法(模板):
@Component
@RequiredArgsConstructor
public class OrderHandlerV3 extends BaseHandler
private final CurrencyService currencyService;
private final EbcService ebcService;
private final OrderDao orderDao;
private final Validator validator;
@Override
protected Result check(Order doc) {
Result result \= new Result();
result.setSuccess(true);
// 时间检查
boolean timeValid = validator.checkTime(doc.getOrderDate());
if (!timeValid) {
result.setSuccess(false);
result.getErrors().add("订单时间格式错误");
}
if (StringUtils.isBlank(doc.getAgentName())) {
result.setSuccess(false);
result.getErrors().add("代理人为空");
}
// todo:其它格式检查 ...
return result;
}
@Override
protected void init(Order doc) {
doc.setCurrencyName(currencyService.getName(doc.getCurrency()));
doc.setEbcName(ebcService.getName(doc.getEbcCode()));
doc.setStatus(OrderStatus.APPLY.getCode());
doc.setCreateTime(LocalDateTime.now());
// todo:其它字段初始化 ...
}
@Override
protected void save(Order doc) {
orderDao.save(doc);
}
/\*\*
\* 设置推送接口
\*
\* @return
\*/
@Override
protected String getApi() {
return "http://host:port/api/order";
}
}
View Code
7. 适合哪些场景
模板模式适用的三个特点:
a. 业务流程相似。如案例中校验、初始化、保存三个步骤在每个单证中都有。
b. 业务实现时局部有差异。如案例中订单、运单、支付单各自要检查的字段不同,状态初始值不同,保存数据用的dao实例不同。
c. 业务类型多。如果案例中只有一个订单或运单功能,不需要有抽象基类(继承就是为了代码复用,业务流程只有一个单证时没有区别),可以将流程和实现在业务类中一同实现。
8. 怎么理解模板模式
a. 两个关键点是抽象和分层。
b. 总结相同或相似的功能并泛化,用一个更大范围的词语来描述就是抽象。
c. 分层就是将相同或相似的功能放到抽象层,将有差异的部分放到实现层。
d. 比如上班族每天的生活都可以抽象为起床洗漱、早餐、上午工作、午餐、下午工作、回家、晚餐这些步骤,这些泛化的步骤就放抽象层。不同的部分在于不同职业、不同城市的上班族起床洗漱时间地点不同,早餐菜品不同,工作内容不同;这些具体的内容实现代码各不相同,就放到实现层。
本文来自博客园,作者:chyun2011,转载请注明原文链接:https://www.cnblogs.com/cy2011/p/18658995