之前谈到年初公司项目基于 DDD 思想进行领域重构时应用到了门面模式,今天来说说重构时运用到的另一种设计模式——模板方法模式(Template Method Pattern)。

事实上,基于模板编程的思想极为常用,我们在一些通用业务的设计过程中经常会声明一些公用的接口、方法,具体业务逻辑的实现只需要实现接口、往通用方法中填充业务逻辑即可。

第13篇·基于模板方法模式实现DDD领域层通用业务 - 图1

1. 定义

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

2. 类图

在模板方法模式中,有两种角色:

  • 模板角色:定义了通用模板方法,一般被声明为接口或抽象类
    • 模板角色中还可以定义钩子函数,如下面类图中的 templateMethod1 方法,通过属性 hook1 来控制钩子函数的执行
  • 具体角色:实现模板角色的具体类,定义了具体业务逻辑

模板方法模式的类图如下:

第13篇·基于模板方法模式实现DDD领域层通用业务 - 图2

3. 示例

基于 DDD 进行重构时,领域层是其中较重要的一层,它负责实现整个业务域最为基础的功能,同时它也是整个系统中最稳定的一环,一旦确定后续再进行改动的频率会小很多。

领域层中最多的业务一般就是增删改查了(当然还会包括其它业务域特有的核心业务),几乎每个持久化模型都需要定义增删改查的业务。就拿 User 模型来举例子,用户注册时需要对 User 模型进行创建,这就需要定义 User 的创建方法,而它可能包括校验、填充、持久化、创建成功后置业务等逻辑。而如果新增一个会员模型,它同样需要定义创建方法,同时它的业务也可能包括校验、填充、持久化、创建成功后置业务等。

可以看到的是,这两个模型的创建方法都具备相似的业务流程,于是我们可以使用模板方法来对该业务进行重构。

由于 Java 只允许单继承,同时在 Java8 中接口也可以存在默认实现,我习惯于使用接口而不是抽象类来定义模板类,其代码如下:

  1. public interface BaseCreateService<Model> {
  2. // 校验方法
  3. void validate(Model model);
  4. // 填充方法
  5. void fill(Model model);
  6. // 持久化方法
  7. int create(Model model);
  8. // 后置业务(钩子函数)
  9. void postHandler(Model model);
  10. // 控制钩子函数是否执行
  11. default boolean hook() {
  12. return false;
  13. }
  14. // 模板方法
  15. default int doCreate(Model model) {
  16. this.validate(model);
  17. this.fill(model);
  18. int id = this.create(model);
  19. if (this.hook()) {
  20. this.postHandler(model);
  21. }
  22. return id;
  23. }
  24. }

用户注册时的创建方法代码如下:

  1. public class CreateUserService implements BaseCreateService<User> {
  2. @Override
  3. public void validate(User user) {
  4. System.out.println("执行User模型校验方法");
  5. }
  6. @Override
  7. public void fill(User user) {
  8. System.out.println("执行User模型填充方法");
  9. }
  10. @Override
  11. public int create(User user) {
  12. System.out.println("执行User模型持久化方法");
  13. return 0;
  14. }
  15. // 覆盖父类接口默认实现,表示钩子函数需要执行
  16. @Override
  17. public boolean hook() {
  18. return true;
  19. }
  20. @Override
  21. public void postHandler(User user) {
  22. System.out.println("执行User模型后置业务方法,如创建成功后异步通知");
  23. }
  24. }

假设创建会员不需要执行后置业务,那么只需要将 hook 方法的返回改变为 false 就可以控制 postHandler 不执行,其代码如下:

  1. public class CreateMemberService implements BaseCreateService<Member> {
  2. @Override
  3. public void validate(Member member) {
  4. System.out.println("执行Member模型校验方法");
  5. }
  6. @Override
  7. public void fill(Member member) {
  8. System.out.println("执行Member模型填充方法");
  9. }
  10. @Override
  11. public int create(Member member) {
  12. System.out.println("执行Member模型持久化方法");
  13. return 0;
  14. }
  15. }

后续如果再有其他模型需要创建(或其他业务)方法,只需要实现 BaseCreateService 接口,然后在相应的方法中填充具体业务逻辑即可。

最后编写测试代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. CreateUserService createUserService = new CreateUserService();
  4. createUserService.doCreate(new User());
  5. CreateMemberService createMemberService = new CreateMemberService();
  6. createMemberService.doCreate(new Member());
  7. }
  8. }

测试代码输出结果为:

  1. 执行User模型校验方法
  2. 执行User模型填充方法
  3. 执行User模型持久化方法
  4. 执行User模型后置业务方法,如创建成功后异步通知
  5. 执行Member模型校验方法
  6. 执行Member模型填充方法
  7. 执行Member模型持久化方法

4. 使用场景

模板方法模式的使用场景是:

  • 多个业务有基本相同的业务逻辑,同时也可以通过钩子函数控制非专有业务的执行

5. 小结

本文讲述了模板方法模式,它的定义是——定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板方法模式的优点是:

  • 将算法中固定的部分封装到父类中,便于代码复用;将可变的部分声明为抽象方法让子类通过继承来实现,易于扩展
  • 符合开闭原则,抽象方法由子类实现,所以子类可以通过扩展的方式增加相应的功能

模板方法模式的缺点是:

  • 基于抽象类、接口实现,如果父类新增抽象方法,所有的子类都需要做变动
  • 钩子函数是否执行是取决于子类的 hook 属性,由子类影响父类执行,这是一种反向控制,提高代码阅读门槛

6. 参考资料

  • 《设计模式之禅》(第2版)

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。