设计模式——任务树模式


2022.3.23更新

组合模式在很久之前只看过一次,一直没用过,理解的也不是很透彻,最近又看了看,我改造出来的这个设计模式与组合模式可以说是一模一样,所以我造了一个组合模式?不管怎么说,这篇文章写完之后,我对组合模式有了一个很清晰的理解。


不知道有多少是冲着文章题目点进来的,设计模式一共23种,哪里蹦出来个任务树模式?
任务树(TaskTree)模式是我为了解决一个级联操作,在责任链模式和策略模式的基础上改造而来的。

为什么不用现有的设计模式

起初我有考虑过使用责任链模式或者观察者模式,但我面临的问题可以抽象成一个树状的结构,孩子节点需要用到双亲节点产生的数据,类似于一个除根节点外,所有节点入度都为1的有向无环图。显然,种非线性的结构用责任链这种线性结构处理起来并不方便。而每一处理节点所用的数据,是经过上一级处理过的,并不是所有处理节点都使用相同数据,因此使用观察者模式也不合适,更何况观察者模式的不同观察者之间并不存在一种先后次序关系,无法传递中间数据,所以就有了“任务树模式”。

为什么叫任务树

因为树中的每个节点都要执行相应的子任务,孩子节点使用双亲节点产生的结果,兄弟节点之间使用从上级传来的、相同的数据(感觉描述怪怪的,稍后看代码应该就清晰了),最终这颗树完成的是一个任务,所以称它为“任务树”。

与观察者模式、责任链模式对比

责任链模式是将请求者和处理者解耦,一个请求进入责任链后,动态选择在某个能处理这个请求的节点执行,也就是说链中的有些节点时不会真正执行的,而且一个请求进入之后可能知到链尾也没有得到处理。
任务树模式是根据实际需求去构建一个树,请求进入后,树中的每个节点都会按相应顺序执行。

观察者模式中,所有观察者收到的通知是一样的,观察者之间没有鲜明的层级关系,所有观察者可以看作是同时执行,而任务树中有鲜明的层次关系,只有必要的父级节点执行完成,相应的孩子节点才会执行。

具体实现

先上一张类图,这里直接把任务树和策略模式结合到一起了(画的比较随意,可能不是很标准)
202203211529183.jpg
其中Handler可以有多个,实现抽象类TaskTree,是任务树的节点类,用于构建任务树;Strategy接口、Strategy类、Context类属于策略模式,Strategy类有多个,实现Strategy接口,提供构建不同任务树的具体策略。

以学院、专业、班级、用户、考试信息级联删除数据为例,实现一个任务树。假设删除学院信息时,CollegeHandler要给根据学院id,查询出该学院下有哪些专业,然后传给MajorHadnler,删除学院数据;MajorHandler根据上级传来的数据查询专业下有哪些班级,传递给ClazzHandler和ExamHandler,然后删除专业数据;ExamHandler根据班级数据,查询有哪些开始信息,然后删除考试信息;ClazzHandler要查询出班级下有哪些学生,传递给UserHandler,然后删除班级数据;UserHandler根据传来的学生id删除学生数据。单纯语言描述不太直观,放个图:
202203211603498.jpg

TaskTree

import java.util.List;

public abstract class TaskTree {
    private TaskTree child; // 孩子节点
    private TaskTree sibling; // 兄弟节点

    // 节点处理过程中要用到的数据
    protected Long id;
    protected List ids;
    protected Boolean result; // 处理结果


    public TaskTree() {
        this.id = null;
        this.child = null;
        this.ids = null;
        this.sibling = null;
        this.result = true;
    }

    public TaskTree(Long id, List ids) {
        this.id = id;
        this.ids = ids;
        this.result = true;
        this.sibling = null;
        this.child = null;
    }

    public TaskTree child() {
        return this.child;
    }

    public TaskTree sibling() {
        return this.sibling;
    }

    public TaskTree setChild(TaskTree node) {
        this.child = node;
        return node;
    }

    public TaskTree setSibling(TaskTree node) {
        this.sibling = node;
        return node;
    }

    // 传播函数,用于向孩子节点、兄弟节点广播数据
    protected Boolean propagation(TaskTree sibling, TaskTree child,
                                  Long id, List ids,
                                  List nextIds, Boolean result) {
        if(sibling != null && child != null)
            return child.doDelete(null, nextIds, result) && sibling.doDelete(id, ids, result);
        else if(child != null) return child.doDelete(null, nextIds, result);
        else if(sibling != null) return sibling.doDelete(id, ids, result);
        else return result;
    }

    // 业务函数,具体在Handler中实现
    public abstract Boolean doDelete(Long id, List ids, Boolean result);
}

Handler

Hadnler共有5个,分别对应处理学院、专业、班级、用户、考试信息。例子中几个类的实现大同小异,这里就只贴一个ClazzHandler的代码,具体代码见文末下载链接。

import cn.weingxing.task_tree.TaskTree;

import java.util.ArrayList;
import java.util.List;

public class ClazzHandler extends TaskTree {
    public ClazzHandler() {
        super();
    }

    public ClazzHandler(Long id, List ids) {
        super(id, ids);
    }

    @Override
    public Boolean doDelete(Long id, List ids, Boolean result) {
        TaskTree sibling = super.sibling();
        TaskTree child = super.child();
        System.out.println("\n\nClazzHandler:");
        // 如果是根节点,这里是个例子,具体如何判断要根据实际业务来定
        if (ids == null) {
            System.out.println("根据ID查询下级所需id数据:" + id);
            List<Integer> nextIds = new ArrayList<Integer>();
            nextIds.add(2122);
            nextIds.add(3313);
            // 模拟操作成功,记录结果
            result = result && true;
            System.out.println("根据ID查询并删除班级数据:" + id);
            return super.propagation(sibling, child, id, ids, nextIds, result);
        } else {
            // 不是根节点
            System.out.println("根据ids查询下级所需数据");
            List<Integer> nextIds = new ArrayList<Integer>();
            nextIds.add(2612);
            nextIds.add(3351);
            nextIds.add(2253);
            // 模拟操作成功,记录结果
            result = result && true;
            System.out.println("根据上级传来的ids删除多条班级数据:");
            for(Object i : ids) {
                System.out.print(i + "\t");
            }
            return super.propagation(sibling, child, id, ids, nextIds, result);
        }
    }
}

使用任务树

实际应用中,可能需要构建不同的任务树,可以和策略模式结合,进一步解耦,这里只做一个演示,就不使用策略模式了,直接在主函数中建立一个任务树。

import cn.weingxing.task_tree.handler.*;

public class Main {
    public static void main(String[] args) {
        TaskTree tree = new CollegeHandler();  // 保留树根
        // 构建任务树
        tree.setChild(new MajorHandler())
                .setChild(new ClazzHandler())
                .setChild(new ExamHandler())
                .setSibling(new UserHandler());

        System.out.println("\n\n最终结果:" + tree.doDelete(1L, null, true));
    }
}

运行结果

CollegeHandler:
根据ID查询下级id数据:1
根据ID查询并删除学院数据:1


MajorHandler:
根据ids查询下级所需数据
根据上级传来的ids删除多条专业数据:
222    333    

ClazzHandler:
根据ids查询下级所需数据
根据上级传来的ids删除多条班级数据:
2212    3231    2223    

ExamHandler:
根据ids查询下级所需数据
根据上级传来的ids删除多条考试数据:
2612    3351    2253    

UserHandler:
根据ids查询下级所需数据
根据上级传来的ids删除多条用户数据:
2612    3351    2253    

最终结果:true

其中如果某一节点执行结果为false,那最终结果也将会是false,如果这个任务是原子操作,结果为false时我们就可以愉快地进行回滚了。

总结

  1. 以上面的例子为例,这样实现之后,如果又引入了新的数据表,比如用户成绩表,在删除用户时要级联删除,那么只要新建一个Handler即可,无需对原有代码做过多修改,提高了可扩展性,降低了对象之间的耦合度,可以根据需要增加新的请求处理类,满足开闭原则。
  2. 各个Handler只负责自己的工作,符合单一职责原则
  3. 只需要维护各节点的孩子、兄弟指针,避免了大片的if...else...语句

缺点:

  1. 构建任务树之前要理清先后逻辑
  2. 树过于庞大时会影响系统性能
  3. 任务树构建不当时会导致循环调用

代码下载:点击查看

声明:迟於|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 设计模式——任务树模式


栖迟於一丘