JavaScript is required
Back

代码精简之路-模板模式

2025/01/09

代码精简之路-模板模式

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 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; }

View Code

5. 改进一一模板模式

a. 实现业务抽象,建立对检验、初始化、保存的标准流程。

b. 实现代码分层,抽象基类负责定义标准流程,实现类负责各业务功能具体实现。上层(抽象基类)负责制定标准,下层负责执行标准。

基类代码:

/** * 抽象基类,定义处理流程 * * @param */ 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());
    }
    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 { @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();

}

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




转载声明
本文内容出自网络,非原创作品。由于无法确认原始来源和作者信息,在此对原作者表示感谢。
如涉及版权问题,请联系 [联系邮箱],我们将及时处理。