【问题标题】:Design pattern for managing groups of tasks用于管理任务组的设计模式
【发布时间】:2023-03-25 11:13:01
【问题描述】:

我有一个处理Task 对象的系统,现在我想做一些基准测试实验。具体来说,我将创建许多 (~100) 个Task 对象,每个对象都属于一个任务组,并且我想在整个任务组上运行系统。我想要一种可以轻松创建新Task 并将其与组关联的设计(轻松使基准套件多样化)。只有少数几个组,因此可以接受一些针对每个组的基础架构。

Tasks 可以包含任意的Objects,所以我不能从像 JSON 这样的“数据”文件类型中加载它们——只有 Java 代码足以创建Tasks。此外,为了可维护性,我想在单独的 Java 文件中创建每个 Task

// SolveHaltingProblem.java
public class SolveHaltingProblem {
    static Task getTask() {
        Task task = new Task();
        task.setName("SolveHaltingProblem");
        ... // Create task
        return task;
    }

    static String getGroup() {
        return "impossible-tasks";
    }
}

那么,Tasks 的群组应该很容易聚集:

List<Task> tasks = gatherTasksInGroup("impossible-tasks");

没有一些愚蠢和容易出错的东西:

List<Task> gatherTasksInGroup(String groupName) {
    List<Task> list = new ArrayList<>();
    if (groupName.equals("impossible-tasks")) {
        list.add(SolveHaltingProblem.getTask());
        list.add(SomeOtherHardProblem.getTask());
        ...
    } else if (...) {
        ... // ~100 more lines
    }
    return list;
}

我提供上述代码只是为了帮助传达我的需求,设计细节并不是一成不变的。也许最好让 SolveHaltingProblem 扩展 ImpossibleTaskGroup 扩展 TaskGroup...

我知道类向其他类注册自己的模式(有这个名称吗?),但我认为该模式不适用于这里,因为我没有创建 SolveHaltingProblem 的实例,并且因此我不能强制SolveHaltingProblem 的任何静态初始化程序运行。我也尝试在 StackOverflow 上搜索类似的问题,但我发现这个问题很难描述;如果这是重复的,我深表歉意。

总而言之,我的问题是:如何管理Tasks 组,以便轻松添加新Task 并将其与组关联?一个好的设计应该具有以下属性(从最重要到最不重要):

  1. 每个Task 都在一个单独的文件中创建
  2. 添加新的Task 并将其与组关联只涉及添加/更改一个文件
  3. 我们只为请求的组构造 Task 对象
  4. 通过复制/粘贴现有​​组的基础架构并稍作修改,“轻松”创建一个新组
  5. 我们不会遍历类路径中的所有类,也不会遍历文件系统中的文件

【问题讨论】:

  • 我不明白 SolveHaltingProblem 类的用法,为什么不只使用 Task 实例? Group 今天只是一个字符串,因此它可以成为 Task 的成员,或者您可以采取完全不同的路线,将 Group 变成一个在集合中包含多个 Task 对象的类。我会考虑使用工厂类或因子方法来创建任务
  • @JoakimDanielson 我猜 SolveHaltingProblem 类并不是绝对必要的,但它是希望在单独的文件中定义每个任务的副作用(以避免过长的 Java 文件并且必须 Ctrl-F 来修改特定任务)。当然,每个 Java 文件都必须定义一个单独的类(例如 SolveHaltingProblem)。我也不认为工厂模式适用,因为我不是一次要求一个任务(我想要一个组中的所有任务,而没有明确列出在该组中构造任务的每个类/文件/方法)。
  • 你需要了解类和类的实例、对象的区别。您需要为您的 Task 类创建一个文件,然后您可以根据需要从该类创建任意数量的不同对象,除了创建这些对象的代码之外不需要任何其他文件。
  • 我明白其中的区别。也许我应该指出 Task 对象非常复杂,每个 Task 需要 20-50 行代码来设置(不,没有更简单的方法来创建它们)。因为“创建这些对象的代码”很复杂,我想将它们分成不同的文件(或者至少是不同的方法)。
  • 这个问题正在Meta讨论

标签: java design-patterns


【解决方案1】:

一种解决方案是使用Composite Pattern,其中接口为Task,终端类为ExecutableTask(您的问题中Task 是什么),复合类为TaskGroup。一个示例实现如下:

public interface Task {
    public void execute();
}

public class ExecutableTask implements Task {

    @Override
    public void execute() {
        // Do some work
    }
}

public class TaskGroup implements Task {

    private final String name;
    private final List<Task> tasks = new ArrayList<>();

    public TaskGroup(String name) {
        this.name = name;
    }

    @Override
    public void execute() {
        for (Task task: tasks) {
            task.execute();
        }
    }

    public void addTask(Task task) {
        tasks.add(task);
    }

    public String getName() {
        return name;
    }
}

这将允许您以与执行单个任务完全相同的方式执行任务组。例如,假设我们有一些方法 benchmarkTask 进行基准测试,我们可以提供 ExecutableTask(单个任务)或 TaskGroup

public void benchmarkTask(Task task) {

    // Record start time

    task.execute();

    // Record completion time
}

// Execute single task
Task oneOffTask = new ExecutableTask();
benchmarkTask(oneOffTask);

// Execute a group of tasks
TaskGroup taskGroup = new TaskGroup("someGroup");
taskGroup.addTask(new ExecutableTask());
taskGroup.addTask(new ExecutableTask());
benchmarkTask(taskGroup);

还可以通过创建各种Task 实现(或扩展ExecutableTask)来扩展此模式,以匹配可以完成的不同任务。例如,假设我们有两种不同类型的任务:颜色任务和定时任务:

public class ColorTask implements Task {

    private final String color;

    public ColorTask(String color) {
        this.color = color;
    }

    @Override
    public void execute() {
        System.out.println("Executed task with color " + color);
    }
}

public class TimedTask implements Task {

    private final long seconds;

    public TimedTask(int seconds) {
        this.seconds = seconds * 1000;
    }

    @Override
    public void execute() {
        Thread.sleep(seconds * 1000);
        System.out.println("Executed timed task for " + seconds + " seconds");
    }
}

然后您可以创建一个包含不同 Task 实现的 TaskGroup 并在没有任何类型特定逻辑的情况下运行它们:

TaskGroup group = new TaskGroup("differentTasksGroup");
group.addTask(new ColorTask("red"));
group.addTask(new TimedTask(2));
group.execute();

这将导致以下输出:

Executed task with color red
Executed timed task for 2 seconds

加载新任务

有了这个复合结构,你就可以使用Java提供的Service Provider Interface (SPI)了。虽然这里不做详细介绍(链接页面包含大量关于如何设置和注册服务的知识),大体思路是可以让Task接口充当服务接口,然后加载各种您注册为服务的任务。例如:

public class TaskServiceMain {

    public static void main(String[] args) {
        TaskGroup rootGroup = new TaskGroup("root");
        ServiceLoader serviceLoader = ServiceLoader.load(Task.class);

        for (Task task: serviceLoader) {
            rootGroup.addTask(task);
        }

        // Execute all of the tasks
        rootGroup.execute();
    }
}

使用复合层次结构的根(Task 接口)作为服务接口可能会很奇怪,因此创建一个新的服务接口可能是个好主意。例如,可以创建一个新界面,它只是充当一个不添加新功能的标记界面:

public interface ServiceTask extends Task {}

则服务加载逻辑变为:

public class TaskServiceMain {

    public static void main(String[] args) {
        TaskGroup rootGroup = new TaskGroup("root");
        ServiceLoader serviceLoader = ServiceLoader.load(ServiceTask.class);

        for (ServiceTask task: serviceLoader) {
            rootGroup.addTask(task);
        }

        // Execute all of the tasks
        rootGroup.execute();
    }
}

【讨论】:

  • 感谢您的回答——这是我以前从未见过的漂亮模式。但是您能否阐明这如何帮助解决我的核心问题,即创建一个新任务并将其添加到一个组中应该只涉及更改/添加一个文件?我已经更多地考虑过这个问题,我认为这只能通过从类加载器中迭代类来通过反射来解决(让每个任务创建类在静态初始化程序中注册一个组,并强制这些类被初始化通过反射)。
  • 在您发表评论后,我绝对理解您的目标。我更新了上面的答案(Loading New Tasks 部分),它解释了如何将服务注册与复合模式联系起来。如果这能回答您的问题,请告诉我。
  • 我读了一些关于 SPI 的文章(对我来说也是新的,谢谢!),似乎注册服务提供者(在这种情况下,一个新任务)涉及将新类的名称添加到某些配置中文件。对我来说,这与原始问题中的“愚蠢且容易出错”的代码示例一样烦人/容易出错。我想我已经找到了自己的解决方案来解决这个问题,我会尽快将其作为答案发布。我会欢迎你的 cmets/反馈我的方法!
【解决方案2】:

在这里,回答他们自己的问题。

我最终使用了一种方法,其中每个 Task-创建类 (TaskCreator) 在类的静态初始化程序中注册自己的组,并使用反射显式初始化这些类。

我使用 TaskGroup.java 中的 Google Guava Reflection 库来迭代类,因为我已经在其他地方依赖于 Guava。 this question 中介绍了其他方法。

这种方法成功地实现了我既定目标的前 4 个,但没有实现第 5 个。 (可能不可能同时满足所有 5 个目标。)

这种方法的优点是:

  • 每个Task 在单独的文件中创建,例如HaltingProblem.javaPerformAddition.java目标 1
  • 创建Task 并将其与组关联仅涉及 1 个文件目标 2
  • Task 对象仅在请求时创建,在 TaskGroup.getTasks() 中(目标 3
  • 创建一个新组是一行更改,即在TaskGroup.java 中添加一个枚举值(目标 4
  • 通过复制/粘贴现有​​的TaskCreator 创建一个新的TaskCreator 非常不言自明,很难搞砸
  • 每个TaskCreator 都可以很明显地将其Task 分配给任意数量的组
  • 我们可以遍历所有组(例如Main.java
  • 我们通过其TaskCreator 处理每个TaskMain.java 中的示例)
  • 编译器确保只能将任务注册到存在的组(使用 String 作为组名时不会发生这种情况)

一些缺点是:

  • 我们不满足目标 5——TaskGroup 的静态初始化器循环遍历类路径中的类
  • 在不依赖某些反射库的情况下实现这种方法很棘手
  • 如果每个 Task 位于多个组中,则可以多次创建它(尽管可以实施缓存机制来解决此问题)
  • 这种方法严重依赖静态初始化器,通常不受欢迎
  • 每个TaskCreator都是单例,一般不喜欢

代码示例

零件是inspired by this answer

Task.java

package my.packagename;

public class Task {
    private String name;
    // and other data

    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    // and other getters/setters
}

TaskCreator.java

package my.packagename;

import my.packagename.Task;

public interface TaskCreator {
    public abstract Task createTask();
}

TaskGroup.java

package my.packagename;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.*;

import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;

public enum TaskGroup {
    IMPOSSIBLE,
    EASY,
    COOL;

    static {
        Class<?> creatorBase = TaskCreator.class;
        try {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            Set<ClassInfo> infoSet = ClassPath.from(loader).getTopLevelClassesRecursive("my.packagename");
            for (ClassInfo info : infoSet) {
                Class<?> cls = info.load();
                if (creatorBase.isAssignableFrom(cls) && !Modifier.isAbstract(cls.getModifiers()))
                    Class.forName(cls.getName()); // runs static initializer for cls
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private final List<TaskCreator> creators = new ArrayList<>();

    public void register(TaskCreator task) {
        creators.add(task);
    }

    public List<Task> getTasks() {
        List<Task> tasks = new ArrayList<>();
        for (TaskCreator creator : creators)
            tasks.add(creator.createTask());
        return tasks;
    }
}

HaltingProblem.java

package my.packagename;

public enum HaltingProblem implements TaskCreator {
    INSTANCE;
    static {
        TaskGroup.IMPOSSIBLE.register(INSTANCE);
        TaskGroup.COOL.register(INSTANCE);
    }

    public Task createTask() {
        Task task = new Task();
        task.setName("Halting problem");
        // and other halting-problem-specific Task setup
        return task;
    }
}

PerformAddition.java

package my.packagename;

public enum PerformAddition implements TaskCreator {
    INSTANCE;
    static {
        TaskGroup.EASY.register(INSTANCE);
        TaskGroup.COOL.register(INSTANCE);
    }

    public Task createTask() {
        Task task = new Task();
        task.setName("Perform addition");
        // and other addition-specific Task setup
        return task;
    }
}

Main.java

package my.packagename;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Task> impossible = TaskGroup.IMPOSSIBLE.getTasks();
        for (Task task : impossible)
            System.out.println("Impossible task: " + task.getName());
        System.out.println();

        List<Task> easy = TaskGroup.EASY.getTasks();
        for (Task task : easy)
            System.out.println("Easy task: " + task.getName());
        System.out.println();

        List<Task> cool = TaskGroup.valueOf("COOL").getTasks();
        for (Task task : cool)
            System.out.println("Cool task: " + task.getName());
        System.out.println();

        // Can iterate through groups
        for (TaskGroup group : TaskGroup.values())
            System.out.println("Group " + group + " has " + group.getTasks().size() + " task(s)");

        // Can easily get a specific Task through its creator
        Task haltingProblemTask = HaltingProblem.INSTANCE.createTask();
    }
}

【讨论】:

    猜你喜欢
    • 2016-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-23
    • 2016-03-29
    • 2015-01-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多