别傻傻的复制粘贴了,用模板方法设计模式优化一下代码吧

676 阅读5分钟

一、先说点什么吧

先聊聊基本定义

模板方法模式属于行为设计模式,它定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

废话不多,先贴源码。

今天的源码是来自某 104.8K43.5K 的知名开源项目:

1.1 片段1

@PostMapping("/create")
@Operation(summary = "新增用户")
@PreAuthorize("@ss.hasPermission('system:user:create')")
public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
    Long id = userService.createUser(reqVO);
    return success(id);
}

@PutMapping("update")
@Operation(summary = "修改用户")
@PreAuthorize("@ss.hasPermission('system:user:update')")
public CommonResult<Boolean> updateUser(@Valid @RequestBody UserSaveReqVO reqVO) {
    userService.updateUser(reqVO);
    return success(true);
}

@DeleteMapping("/delete")
@Operation(summary = "删除用户")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:user:delete')")
public CommonResult<Boolean> deleteUser(@RequestParam("id") Long id) {
    userService.deleteUser(id);
    return success(true);
}

这是用户的 Controller,然后其他的 Controller 也几乎都有这些方法。

1.2 片段2

@RequiresPermissions("system:user:add")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(@Validated SysUser user)
{
    deptService.checkDeptDataScope(user.getDeptId());
    roleService.checkRoleDataScope(user.getRoleIds());
    if (!userService.checkLoginNameUnique(user))
    {
        return error("新增用户'" + user.getLoginName() + "'失败,登录账号已存在");
    }
    user.setSalt(ShiroUtils.randomSalt());
    user.setPassword(passwordService.encryptPassword(user.getLoginName(), user.getPassword(), user.getSalt()));
    user.setPwdUpdateDate(DateUtils.getNowDate());
    user.setCreateBy(getLoginName());
    return toAjax(userService.insertUser(user));
}

@RequiresPermissions("system:user:edit")
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(@Validated SysUser user)
{
    userService.checkUserAllowed(user);
    userService.checkUserDataScope(user.getUserId());
    deptService.checkDeptDataScope(user.getDeptId());
    roleService.checkRoleDataScope(user.getRoleIds());
    if (!userService.checkLoginNameUnique(user))
    {
        return error("修改用户'" + user.getLoginName() + "'失败,登录账号已存在");
    }
    user.setUpdateBy(getLoginName());
    AuthorizationUtils.clearAllCachedAuthorizationInfo();
    return toAjax(userService.updateUser(user));
}

和 片段1 的一样,其他模块也几乎是这么写的,而且都有诸如 user.setUpdateBy(getLoginName()); 子类的语句调用……

看了直接上脑。直接上脑。 老规矩,大伙都可以在评论区聊聊这些代码。

二、思考和改进

我们说了模板方法设计模式,那么如何用设计模式来优化这些代码呢?

2.1 模板方法优化增删改查

先简单分析一下:

像这类的增删改查,大都是通用的。比如 新增:“一般都是先判断能不能新增,再执行新增,然后再执行新增完毕之后的其他操作,然后返回新增的主键ID。”,

有了分析,那么我们可以对这类代码进行改造了:

先在基类中写好 add() 方法,然后给这个方法添加一些固定的步骤,比如 beforeAdd()afterAdd()

改造后:

// AbstractBaseController.java
@PostMapping("add")
public Json add(@RequestBody @Validated(WhenAdd.class) E source) {
  source = beforeAdd(source);
  long id = service.add(source);
  afterAdd(id, source)
  return Json.entity(id, "添加成功");
}

protected abstract E beforeAdd(E source);

protected abstract void afterAdd(long id, E source);

// 其他需要实现的抽象方法

那么,接下来 UserController 就可以继承这个基类后,自己去实现添加前后的业务代码即可:

// UserController.java
@Override
protected User beforeAdd(User source) {
  if(source.getAge() < 18){
    throw new IllegalArgumentException("用户必须年满18岁");
  }
  return source;
}

@Override
protected void afterAdd(long id, User source) {
  // 发送邮件通知用户,用户注册成功
  emailService.send("用户注册成功,用户ID: " + id + "\n用户名:" + source.getName());
}

当然,还有很多的业务模块在 add() 前后并不是一定有业务逻辑,所以我们可以直接将 add()前置和后置方法 从抽象方法直接改为默认实现,这样其他没必要的子类就可以不用实现这个方法了:

// AbstractBaseController
@PostMapping("add")
public Json add(@RequestBody @Validated(WhenAdd.class) E source) {
  source = beforeAdd(source);
  long id = service.add(source);
  afterAdd(id, source)
  return Json.entity(id, "添加成功");
}

protected E beforeAdd(E source){
// 默认实现 直接返回
  return source
}

protected void afterAdd(long id, E source){
  // 默认空业务
}

使用的时候就更简单了:

// RoleController.java
@RestController("role")
class RoleController extends AbstractController<RoleEntity, RoleService> {
  // 没有新增的前后特殊业务 啥也不写。

  // 其他的自定义业务
  @PostMapping("authorize")
  public Json authorize(@RequestBody @Validated(WhenAuthorize.class) E role){
    // do something
  }
}

2.2 模板方法默认实现

使用模板方法不仅可以用来做一些类似 前后置 处理的应用场景,还可以用来做一些默认实现。

比如我们正在 core 包中封装统一的导出数据的业务代码,我们希望其他业务系统使用我们统一的导出业务逻辑,但又会提供给他们一些可以自定义的项目。

于是我们着手这么设计:

class BaseService<E> {
  public final String export(ExportParam<E> param){
    // 先查出需要导出的数据
    List<E> list = exportQuery(queryListRequest);
    // 创建导出的流
    InputStream inputStream = createExportStream(list);
    // 将流保存到文件后返回文件的路径
    return saveExportFile(inputStream);
  }

  protected final List<E> exportQuery(ExportParam<E> param){
    // 查询数据并返回
    return query(param)
  }

  protected InputStream createExportStream(List<E> list){
    // 创建一个文件 默认使用 csv 格式
    /*
    伪代码,生成csv文件的流并返回
     */
  }

  protected String saveExportFile(InputStream inputStream){
    // 保存文件到服务器指定目录后返回文件路径
  }
}

那么,接下来其他的服务只需要继承 BaseService 就拥有了导出的能力,默认是csv格式的表格,默认是存储到服务器中的。

突然有一天,账单的导出需要修改为 xls 格式,并且需要把文件存储到 oss 上。其他的保持不变。

小场面,那我们只需要重写 账单Service 的 createExportStreamsaveExportFile 方法:

// PaymentService.java
class PaymentService extends BaseService<Payment> {
  @Override
  protected InputStream createExportStream(List<Payment> list) {
    // 这里是将查到的List存储到 **xls** 文件中,并返回文件流
  }

  @Override
  protected String saveExportFile(InputStream inputStream) {
    // 这里是将流存储到 OSS 中 并返回 OSS里的URL路径即可。
  }
}

美滋滋,最小改动。

2.3 模板方法强制子类实现

如果模板方法是需要子类强制实现的,不是上面的默认实现,那么配合抽象类,可以完成的功能就更多了。

比如流程控制场景,某些必须走的流程 step1,step2,step3 那么父类中直接按流程写好三个方法的调用顺序,并提供3个抽象方法,让子类去实现,这样子类就需要强制实现了这3个步骤,父类只管了整个流程,具体的实现逻辑就交给子类了。

这里就不举例了,本文主要还是讨论了第一部分贴的代码的一些优化方式。

三、思考和总结

我喜欢没事就读读别人的代码,学学人家的设计思维,所以读了很多有意思的代码,也就有了第一部贴的这些代码。

今天的吐槽就到这吧。

如果你在写 JavaSpringBoot,那这里分享一下我们这个开源项目:

AirPower4J on Github: github.com/HammCn/AirP…

也欢迎你来贴出我们的代码,欢迎吐槽。

That's all.