代码精简之路-模板模式
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; } }
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; }
格式检查代码封装,实现多种格式的时间检查:

public boolean checkTime(String time) { List<String> formatters = new ArrayList<>(); formatters.add("yyyy-MM-dd HH:mm:ss"); formatters.add("yyyy年MM月dd日 HH时mm分ss秒"); formatters.add("yyyyMMddHHmmss"); formatters.add("yyyy/MM/dd HH:mm:ss"); boolean pass = formatters.stream().anyMatch(format -> { try { LocalTime parse = LocalTime.parse(time, DateTimeFormatter.ofPattern(format)); return true; } catch (DateTimeParseException e) { return false; } }); return pass; }
5. 改进一一模板模式
a. 实现业务抽象,建立对检验、初始化、保存的标准流程。
b. 实现代码分层,抽象基类负责定义标准流程,实现类负责各业务功能具体实现。上层(抽象基类)负责制定标准,下层负责执行标准。
基类代码:

/** * 抽象基类,定义处理流程 * * @param <T> */ public abstract class BaseHandler<T> { /** * 接收申报处理入口:校验、赋初值、保存都在这里实现了,下层类不需要写流程处理的重复代码 * * @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); }
改进订单处理代码,只需填充基类模板空出来的3个方法:

@Component @RequiredArgsConstructor public class OrderHandlerV3 extends BaseHandler<Order> { 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); } }
改进运单处理代码,只需填充基类模板空出来的3个方法:

@Component @RequiredArgsConstructor public class WaybillHandlerV3 extends BaseHandler<Waybill> { 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); } }
6. 优劣对比
a. 有利于阅读代码、维护功能。
-
原始代码中3个步骤(校验、赋初始值、保存)的功能在混合交叉在一起,在一个方法中实现,阅读维护非常耗时。将来如果需求变动,如字段长度变化/必填字段变化要修改数据检查部分代码;状态字段值变化(申报由1表示改为由A表示)而修改赋初始值部分代码;ORM框架变化修改dao的实例。这时就只能到这一个方法中寻找对应部分,要从头到尾阅读代码。
- 模板模式中实现对业务抽象、建立流程以后,代码结构层次清晰,只要需到抽象类或实现类的对应流程中去寻找修改。
b. 有利于功能升级。现在的功能只有3步,假如将来功能拓展,如对接别的系统平台(把合规的数据转为json格式推送给目标系统的接口)。
- 在原始代码中就需要分别到各个单证类中分别添加数据格式转换、推送接口的功能,再分别测试。
- 在模板模式代码中只需要求在基类的流程中再加两个步骤,甚至转换和推送都可以在基类中统一实现,相比之下编码和测试都减少了。
c. 功能升级举例,流程处理中增加推送功能:
- 格式转换和推送都在基类中完成。
- 各实现类中只需设置不同单证的推送接口。
升级后的基类,只增加了4行代码:

public abstract class BaseHandler<T> { @Autowired private ApiClient apiClient; /** * 接收申报处理入口:校验、赋初值、保存都在这里实现了,下层类不需要写流程处理的重复代码 * * @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(); }
升级后的订单处理类,只填充接口地址方法(模板):

@Component @RequiredArgsConstructor public class OrderHandlerV3 extends BaseHandler<Order> { 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"; } }
7. 适合哪些场景
模板模式适用的三个特点:
a. 业务流程相似。如案例中校验、初始化、保存三个步骤在每个单证中都有。
b. 业务实现时局部有差异。如案例中订单、运单、支付单各自要检查的字段不同,状态初始值不同,保存数据用的dao实例不同。
c. 业务类型多。如果案例中只有一个订单或运单功能,不需要有抽象基类(继承就是为了代码复用,业务流程只有一个单证时没有区别),可以将流程和实现在业务类中一同实现。
8. 怎么理解模板模式
a. 两个关键点是抽象和分层。
b. 总结相同或相似的功能并泛化,用一个更大范围的词语来描述就是抽象。
c. 分层就是将相同或相似的功能放到抽象层,将有差异的部分放到实现层。
d. 比如上班族每天的生活都可以抽象为起床洗漱、早餐、上午工作、午餐、下午工作、回家、晚餐这些步骤,这些泛化的步骤就放抽象层。不同的部分在于不同职业、不同城市的上班族起床洗漱时间地点不同,早餐菜品不同,工作内容不同;这些具体的内容实现代码各不相同,就放到实现层。
本文来自博客园,作者:chyun2011,转载请注明原文链接:https://www.cnblogs.com/cy2011/p/18658995
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)