访客模式在这种情况下有效吗?


9

我任务的目标是设计一个可以运行计划的定期任务的小型系统。重复执行的任务类似于“星期一至星期五,从上午8:00到下午5:00,每小时发送一封电子邮件给管理员”。

我有一个名为RecurringTask的基类。

public abstract class RecurringTask{

    // I've already figured out this part
    public bool isOccuring(DateTime dateTime){
        // implementation
    }

    // run the task
    public abstract void Run(){

    }
}

我有几个从RecurringTask继承的类。其中之一称为SendEmailTask

public class SendEmailTask : RecurringTask{
    private Email email;

    public SendEmailTask(Email email){
        this.email = email;
    }

    public override void Run(){
        // need to send out email
    }
}

我有一个EmailService,可以帮助我发送电子邮件。

最后一个类是RecurringTaskScheduler,它负责从缓存或数据库中加载任务并运行任务。

public class RecurringTaskScheduler{

    public void RunTasks(){
        // Every minute, load all tasks from cache or database
        foreach(RecuringTask task : tasks){
            if(task.isOccuring(Datetime.UtcNow)){
                task.run();
            }
        }
    }
}

这是我的问题:我应该将EmailService放在哪里

选项1:将EmailService注入SendEmailTask

public class SendEmailTask : RecurringTask{
    private Email email;

    public EmailService EmailService{ get; set;}

    public SendEmailTask (Email email, EmailService emailService){
        this.email = email;
        this.EmailService = emailService;
    }

    public override void Run(){
        this.EmailService.send(this.email);
    }
}

关于是否应该将服务注入实体,已经有一些讨论,并且大多数人都认为这不是一个好习惯。看到这篇文章

选项2: If ... Else在RecurringTaskScheduler中

public class RecurringTaskScheduler{
    public EmailService EmailService{get;set;}

    public class RecurringTaskScheduler(EmailService emailService){
        this.EmailService = emailService;
    }

    public void RunTasks(){
        // load all tasks from cache or database
        foreach(RecuringTask task : tasks){
            if(task.isOccuring(Datetime.UtcNow)){
                if(task is SendEmailTask){
                    EmailService.send(task.email); // also need to make email public in SendEmailTask
                }
            }
        }
    }
}

有人告诉我If ... Else和上面的转换不是OO,这会带来更多问题。

选项3:更改运行的签名并创建ServiceBundle

public class ServiceBundle{
    public EmailService EmailService{get;set}
    public CleanDiskService CleanDiskService{get;set;}
    // and other services for other recurring tasks

}

将此类注入RecurringTaskScheduler

public class RecurringTaskScheduler{
    public ServiceBundle ServiceBundle{get;set;}

    public class RecurringTaskScheduler(ServiceBundle serviceBundle){
        this.ServiceBundle = ServiceBundle;
    }

    public void RunTasks(){
        // load all tasks from cache or database
        foreach(RecuringTask task : tasks){
            if(task.isOccuring(Datetime.UtcNow)){
                task.run(serviceBundle);
            }
        }
    }
}

SendEmailTaskRun方法将是

public void Run(ServiceBundle serviceBundle){
    serviceBundle.EmailService.send(this.email);
}

我看不到这种方法有什么大问题。

选项4:访客模式。
基本思想是创建一个访问者,该访问者将像ServiceBundle一样封装服务。

public class RunTaskVisitor : RecurringTaskVisitor{
    public EmailService EmailService{get;set;}
    public CleanDiskService CleanDiskService{get;set;}

    public void Visit(SendEmailTask task){
        EmailService.send(task.email);
    }

    public void Visit(ClearDiskTask task){
        //
    }
}

并且我们还需要更改Run方法的签名。SendEmailTaskRun方法是

public void Run(RecurringTaskVisitor visitor){
    visitor.visit(this);
}

这是访客模式的典型实现,并且访客将被注入RecurringTaskScheduler中

总结:在这四种方法中,哪种方法最适合我的情况?在此问题上,选项3和选项4之间有什么大区别吗?

还是您对此问题有更好的主意?谢谢!

2015年5月22日更新:我认为Andy的回答很好地总结了我的意图;如果您仍然对问题本身感到困惑,建议您先阅读他的文章。

我刚刚发现我的问题与Message Dispatch问题非常相似,后者导致了Option5。

选项5:将我的问题转换为消息调度
我的问题和消息调度问题之间存在一对一的映射:

消息调度:接收即时聊天和调度子类的即时聊天到其相应的处理程序。→RecurringTaskScheduler

IMessage:接口或抽象类。→重复任务

MessageA:从IMessage扩展,具有一些其他信息。→SendEmailTask

MessageBIMessage的另一个子类。→CleanDiskTask

MessageAHandler:接收到MessageA时,对其进行处理→SendEmailTask​​Handler,其中包含EmailService,并且在接收到SendEmailTask​​时将发送一封电子邮件

MessageBHandler:与MessageAHandler相同,但改为处理MessageB。→CleanDiskTaskHandler

最困难的部分是如何将不同类型的IMessage调度到不同的处理程序。这是一个有用的链接

我真的很喜欢这种方法,它不会因服务而污染我的实体,也没有任何神职人员


您尚未标记语言或平台,但建议您使用cron。您的平台可能具有类似工作的库(例如jcron似乎已失效)。安排工作和任务在很大程度上是一个已解决的问题:在轮换工作之前,您是否曾考虑过其他选择?是否有不使用它们的原因?

@Snowman我们稍后可能会切换到成熟的库。这完全取决于我的经理。我发布此问题的原因是我想找到一种解决此类“问题”的方法。我已经不止一次地看到这种问题,并且找不到完美的解决方案。所以我想知道我做错了什么。
Sher10ck

公平地说,尽管有可能,我总是尝试推荐代码重用。

1
SendEmailTask对我来说,似乎更像是一种服务而不是实体。我会毫不犹豫地选择选项1。
巴特·范·英根·谢瑙

3
(对我而言)Visitor缺少的是访问者的类结构accept。对Visitor的动机是,您在某种聚合中有许多类类型需要访问,并且为每种新功能(操作)修改其代码并不方便。我仍然看不到这些聚合对象是什么,并且认为Visitor不适合。如果是这种情况,则应编辑您的问题(指访问者)。
导演2015年

Answers:


4

我会说选择1是最好的选择。你不应该关闭它的原因是,SendEmailTask不是一个实体。实体是与保存数据和状态有关的对象。您的课程几乎没有。实际上,它不是一个实体,但是它拥有一个实体:Email您正在存储的对象。这意味着不Email应该服务或拥有#Send方法。相反,您应该拥有采用实体的服务,例如EmailService。因此,您已经在遵循将服务置于实体之外的想法。

由于SendEmailTask不是实体,因此将电子邮件和服务注入其中完全可以,这应该通过构造函数来完成。通过进行构造函数注入,我们可以确保SendEmailTask始终准备好执行其工作。

现在让我们看一下为什么不做其他选择(特别是关于SOLID)。

选项2

正确地告诉您,像这样的类型分支将为您带来更多麻烦。让我们看看为什么。首先,ifs趋于聚集和增长。今天,发送电子邮件是一项任务,明天,每种不同类型的课程都需要不同的服务或其他行为。管理该if声明成为噩梦。由于我们是基于类型(在这种情况下为显式类型)分支的,因此我们正在破坏内置在语言中的类型系统。

选项2不是单一职责(SRP),因为以前可重用的RecurringTaskScheduler现在必须了解所有这些不同类型的任务,以及它们可能需要的所有不同种类的服务和行为。该类很难重用。它也不是打开/关闭(OCP)。因为它需要知道这类任务或一项任务(或这种服务或那种服务),所以对任务或服务的不同更改可能会迫使此处进行更改。添加新任务?添加新服务?更改电子邮件的处理方式?改变RecurringTaskScheduler。因为任务的类型很重要,所以它不遵守Liskov Substitution(LSP)。它不能只是完成任务而已。它必须询问类型,并根据类型执行此操作或执行该操作。与其将差异封装到任务中,不如将所有这些都拉进RecurringTaskScheduler

选项3

选项3有一些大问题。即使在您链接到的文章中,作者也不鼓励这样做:

  • 您仍然可以使用静态服务定位器…
  • 我会尽量避免使用服务定位器,尤其是当服务定位器必须是静态的时……

您正在使用您的班级创建服务定位器ServiceBundle。在这种情况下,它似乎不是静态的,但是它仍然存在服务定位器中固有的许多问题。现在,您的依赖项已隐藏在this下ServiceBundle。如果我为您提供了以下很棒的新任务的API:

class MyCoolNewTask implements RecurringTask
{
    public bool isOccuring(DateTime dateTime) {
        return true; // It's always happenin' here!
    }

    public void Run(ServiceBundle bundle) {
        // yeah, some awesome stuff here
    }
}

我正在使用哪些服务?测试中需要模拟哪些服务?是什么原因使我无法使用系统中的每个服务?

如果我想使用您的任务系统来运行某些任务,那么即使我只使用了很少甚至根本不使用,我现在都依赖于系统中的每个服务。

ServiceBundle,因为它需要知道是不是真的SRP 每一个在你的系统服务。它也不是OCP。添加新服务意味着对的更改ServiceBundle,而对的更改ServiceBundle可能意味着对其他位置的任务进行了不同的更改。ServiceBundle不隔离其接口(ISP)。它具有所有这些服务的庞大接口,并且由于它只是这些服务的提供者,因此我们可以考虑将其接口包含为其提供的所有服务的接口。任务不再遵循Dependency Inversion(DIP),因为它们的依赖关系被混淆在后面ServiceBundle。这也不符合最低知识原则(又称得墨meter耳定律),因为事物知道的东西比他们必须知道的还要多。

选项4

以前,您有很多可以独立操作的小对象。选项4接收所有这些对象并将它们粉碎在一起成为单个Visitor对象。该对象充当您所有任务的上帝对象。它可以将您的RecurringTask对象减少为贫瘠的阴影,而这些阴影只需要吸引访客即可。所有行为都移到Visitor。需要改变行为?需要添加新任务吗?改变Visitor

更具挑战性的部分是,因为所有不同的行为都在同一类中,所以沿所有其他行为多态地更改某些拖曳行为。例如,我们希望有两种不同的发送电子邮件的方式(也许应该使用不同的服务器?)。我们将如何做?我们可以创建一个IVisitor接口并实现该接口,并可能复制代码,例如#Visit(ClearDiskTask)从原始访问者那里实现。然后,如果我们提出了一种清除磁盘的新方法,则必须实现并再次复制。然后我们想要两种变化。实施并再次复制。这两种不同的行为是密不可分的。

也许我们可以只是继承它Visitor?具有新电子邮件行为的子类,具有新磁盘行为的子类。到目前为止没有重复!两者都有子类吗?现在,其中一个或另一个需要重复(如果您愿意,可以重复两个)。

让我们与选项1进行比较:我们需要一种新的电子邮件行为。我们可以创建一个RecurringTask执行新行为的新代码,注入其依赖项,并将其添加到中的任务集合中RecurringTaskScheduler。我们甚至不需要谈论清除磁盘,因为这种责任完全在其他地方。我们还可以使用全套的OO工具。例如,我们可以用日志记录来装饰该任务。

选项1会使您的痛苦最小化,并且是处理这种情况的最正确方法。


您对Otion2,3,4的分析非常棒!确实对我有很大帮助。但是对于Option1,我认为* SendEmailTask​​ *是一个实体。它具有id,具有其重复模式,以及其他有用的信息,这些信息应存储在db中。我认为安迪很好地概括了我的意图。也许像* EMailTask​​Definitions *这样的名称更合适。我不想用我的服务代码污染我的实体。如果我将服务注入实体,欣快感就会提到一些问题。我还更新了我的问题,并包括了Option5,我认为这是迄今为止最好的解决方案。
Sher10ck

@ Sher10ck如果您要从SendEmailTask数据库中提取配置,则该配置应该是一个单独的配置类,也应该注入到您的中SendEmailTask。如果要从中生成数据SendEmailTask,则应创建一个memento对象以存储状态并将其放入数据库中。
cbojar 2015年

我需要从数据库中提取配置,所以您建议同时将EMailTaskDefinitionsEmailService注入SendEmailTask吗?然后在中RecurringTaskScheduler,我需要注入类似SendEmailTaskRepository的东西,其职责是加载定义和服务并将其注入SendEmailTask。但是我现在要争论的是RecurringTaskScheduler需要了解每个任务的存储库,例如CleanDiskTaskRepository。而且,我RecurringTaskScheduler每次有新任务时都需要进行更改(将存储库添加到Scheduler中)。
Sher10ck

@ Sher10ck RecurringTaskScheduler应当只知道广义任务存储库和的概念RecurringTask。通过这样做,它可以依赖于抽象。可以将任务存储库注入的构造函数中RecurringTaskScheduler。然后,仅需要知道不同的存储库在哪里RecurringTaskScheduler实例化(或可以隐藏在工厂中并从那里调用)。因为它仅取决于抽象,RecurringTaskScheduler所以不需要随每个新任务而变化。那就是依赖倒置的本质。
cbojar 2015年

3

您是否看过现有的资料库,例如弹簧石英或弹簧批次(我不确定最适合您的需求)?

对你的问题:

我认为问题是,您希望以多态的方式将某些元数据持久化到任务中,因此,电子邮件任务已分配了电子邮件地址,日志任务已分配日志级别,等等。您可以将这些列表存储在内存中或数据库中,但是为了分开考虑,您不希望实体受到服务代码的污染。

我建议的解决方案:

我将任务的运行部分和数据部分分开,以具有eg TaskDefinition和a TaskRunner。TaskDefinition具有对TaskRunner或创建工厂的工厂的引用(例如,如果需要某些设置,例如smtp-host)。工厂是一个特定的工厂-它只能处理EMailTaskDefinitions,并且仅返回EMailTaskRunners的实例。这样,它更加面向对象,并且更安全-如果引入新的任务类型,则必须引入新的特定工厂(或重用工厂),否则,您将无法编译。

这样,您最终会遇到一个依赖关系:实体层->服务层,然后再返回,因为Runner需要存储在实体中的信息,并且可能想要更新其在数据库中的状态。

你可以通过使用一个通用的工厂,这需要打破圆一个 TaskDefinition并返回一个特定 TaskRunner,但这需要大量的IFS的。您可以使用反射来找到一个与您的定义名称类似的运行器,但是请谨慎使用此方法,这会降低性能,并可能导致运行时错误。

PS我在这里假设Java。我认为在.net中类似。这里的主要问题是双重绑定。

到访客模式

我认为它的目的是用于在运行时交换各种数据对象的算法,而不是纯粹的双重绑定目的。例如,如果您拥有不同种类的保险并计算不同种类的保险,例如因为不同国家/地区要求这样做。然后,您选择一种特定的计算方法并将其应用于多种保险。

在您的情况下,您将选择特定的任务策略(例如电子邮件)并将其应用于您的所有任务,这是错误的,因为并非所有任务都是电子邮件任务。

PS我没有测试它,但我认为您的Option 4也不起作用,因为它再次被双重装订了。


您真的很好地总结了我的意图,谢谢!我想打破圈子。因为让TaskDefiniton保留对TaskRunnerFactory的引用与Option1一样存在问题。我将工厂TaskRunner视为服务。如果TaskDefinition需要保留对它们的引用,则可以将服务注入TaskDefinition或使用某些静态方法,而我正试图避免这种方法。
Sher10ck

1

我完全不同意该文章。服务(确切地说是它们的“ API”)是业务域的重要组成部分,因此将存在于域模型中。业务域中的实体引用同一业务域中的其他对象没有任何问题。

当X发送邮件给Y。

是业务规则。为此,需要发送邮件的服务。并且处理的实体When X应了解此服务。

但是实现存在一些问题。对于实体的用户而言,实体正在使用服务应该是透明的。因此,在构造函数中添加服务不是一件好事。当您从数据库反序列化实体时,这也是一个问题,因为您需要同时设置实体的数据和服务实例。我能想到的最佳解决方案是在实体创建后使用属性注入。可能会强制任何实体的每个新创建的实例通过“初始化”方法来注入实体所需的所有实体。


您不同意指什么文章?但是,关于域模型的观点很有趣。也许您可以看到这样的结果,但是人们通常避免将服务混合到实体中,因为这将很快形成紧密的耦合。
安迪

@Andy他的问题中提到的一个Sher10ck。而且我不知道它将如何产生紧密的耦合。任何写得不好的代码都可能导致紧密耦合。
欣快感,2015年

1

这是一个大问题,也是一个有趣的问题。我建议您结合使用责任链双重派遣模式(此处的示例示例)。

首先让我们定义任务层次。请注意,现在有多种run方法可以实现Double Dispatch。

public abstract class RecurringTask {

    public abstract boolean isOccuring(Date date);

    public boolean run(EmailService emailService) {
        return false;
    }

    public boolean run(ExecuteService executeService) {
        return false;
    }
}

public class SendEmailTask extends RecurringTask {

    private String email;

    public SendEmailTask(String email) {
        this.email = email;
    }

    @Override
    public boolean isOccuring(Date date) {
        return true;
    }

    @Override
    public boolean run(EmailService emailService) {
        emailService.runTask(this);
        return true;
    }

    public String getEmail() {
        return email;
    }
}

public class ExecuteTask extends RecurringTask {

    private String program;

    public ExecuteTask(String program) {
        this.program = program;
    }

    @Override
    public boolean isOccuring(Date date) {
        return true;
    }

    public String getName() {
        return program;
    }

    @Override
    public boolean run(ExecuteService executeService) {
        executeService.runTask(this);
        return true;
    }
}

接下来让我们定义Service层次结构。我们将使用Services来形成责任链。

public abstract class Service {

    private Service next;

    public Service(Service next) {
        this.next = next;
    }

    public void handleRecurringTask(RecurringTask req) {
        if (next != null) {
            next.handleRecurringTask(req);
        }
    }
}

public class ExecuteService extends Service {

    public ExecuteService(Service next) {
        super(next);
    }

    void runTask(ExecuteTask task) {
        System.out.println(String.format("%s running %s with content '%s'", this.getClass().getSimpleName(),
                task.getClass().getSimpleName(), task.getName()));
    }

    public void handleRecurringTask(RecurringTask req) {
        if (!req.run(this)) {
            super.handleRecurringTask(req);
        }
    }
}

public class EmailService extends Service {

    public EmailService(Service next) {
        super(next);
    }

    public void runTask(SendEmailTask task) {
        System.out.println(String.format("%s running %s with content '%s'", this.getClass().getSimpleName(),
                task.getClass().getSimpleName(), task.getEmail()));
    }

    public void handleRecurringTask(RecurringTask req) {
        if (!req.run(this)) {
            super.handleRecurringTask(req);
        }
    }
}

最后一块是RecurringTaskScheduler协调装载和运行过程的零件。

public class RecurringTaskScheduler{

    private List<RecurringTask> tasks = new ArrayList<>();

    private Service chain;

    public RecurringTaskScheduler() {
        chain = new EmailService(new ExecuteService(null));
    }

    public void loadTasks() {
        tasks.add(new SendEmailTask("here comes the first email"));
        tasks.add(new SendEmailTask("here is the second email"));
        tasks.add(new ExecuteTask("/root/python"));
        tasks.add(new ExecuteTask("/bin/cat"));
        tasks.add(new SendEmailTask("here is the third email"));
        tasks.add(new ExecuteTask("/bin/grep"));
    }

    public void runTasks(){
        for (RecurringTask task : tasks) {
            if (task.isOccuring(new Date())) {
                chain.handleRecurringTask(task);
            }
        }
    }
}

现在,这里是演示系统的示例应用程序。

public class App {

    public static void main(String[] args) {
        RecurringTaskScheduler scheduler = new RecurringTaskScheduler();
        scheduler.loadTasks();
        scheduler.runTasks();
    }
}

运行应用程序输出:

EmailService运行SendEmailTask与内容“来了第一封电子邮件”
EmailService运行SendEmailTask与内容“这里是第二个电子邮件”
运行ExecuteTask与内容ExecuteService'/根/ Python的
ExecuteService运行ExecuteTask与内容“/斌/猫”
EmailService运行SendEmailTask与内容“这是第三封电子邮件”,
运行带有内容“ / bin / grep”的ExecuteTask 的ExecuteService


我可能有很多任务。每次添加新Task时,都需要更改RecurringTask,还需要更改其所有子类,因为我需要添加一个新函数,例如 public abstract boolean run(OtherService otherService)。我认为同时实现双重调度的访客模式Option4也存在相同的问题。
Sher10ck

好点子。我编辑了答案,以便在RecurringTask中定义run(service)方法,默认情况下返回false。这样,当您需要添加另一个任务类时,您无需触摸同级任务。
iluwatar 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.