The goal of my task is to design a small system which can run scheduled recurring tasks. A recurring task is something like "send an email to administrator every hour from 8:00 am to 5:00 pm, Monday through Friday".
I have a base class called 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(){
}
}
And I have several classes which are inherited from RecurringTask. One of them is called SendEmailTask.
public class SendEmailTask : RecurringTask{
private Email email;
public SendEmailTask(Email email){
this.email = email;
}
public override void Run(){
// need to send out email
}
}
And I have a EmailService which can helps me to send out an email.
The last class is RecurringTaskScheduler, it's responsible for loading tasks from cache or database and run the task.
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();
}
}
}
}
Here is my problem: where should I put EmailService?
Option1: Inject EmailService into 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);
}
}
There are already some discussions on whether we should inject a service into an entity, and most people agree it's not a good practice. See this article.
Option2: If...Else in 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
}
}
}
}
}
I've been told If...Else and cast like above is not OO, and will bring more problems.
Option3: Change the signature of Run and create ServiceBundle.
public class ServiceBundle{
public EmailService EmailService{get;set}
public CleanDiskService CleanDiskService{get;set;}
// and other services for other recurring tasks
}
Inject this class into 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);
}
}
}
}
The Run method of SendEmailTask would be
public void Run(ServiceBundle serviceBundle){
serviceBundle.EmailService.send(this.email);
}
I don't see any big problems with this approach.
Option4: Visitor pattern.
The basic idea is create a visitor which will encapsulate services just like 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){
//
}
}
And we also need to change the signature of Run method. The Run method of SendEmailTask is
public void Run(RecurringTaskVisitor visitor){
visitor.visit(this);
}
It is a typical implementation of Visitor Pattern, and the visitor will be injected into RecurringTaskScheduler.
In summary: Among these four approaches, which one is the best for my scenario? And are there any big difference between Option3 and Option4 for this problem?
Or do you have better idea on this problem? Thanks!
Update 5/22/2015: I think Andy's answer summarizes my intention really well; if you are still confused about the problem itself, I suggest reading his post first.
I just found out my problem is very similar to Message Dispatch problem, which leads to Option5.
Option5: Convert my problem to Message Dispatch.
There is a one-to-one mapping between my problem and Message Dispatch problem:
Message Dispatcher : Receive IMessage and dispatch sub classes of IMessage to their corresponding handlers. → RecurringTaskScheduler
IMessage : An interface or an abstract class. → RecurringTask
MessageA : Extends from IMessage, having some additional information. → SendEmailTask
MessageB : Another subclass of IMessage. → CleanDiskTask
MessageAHandler : When receive MessageA, handle it → SendEmailTaskHandler, which contains EmailService, and will send out an email when it receives SendEmailTask
MessageBHandler : Same as MessageAHandler, but handle MessageB instead. → CleanDiskTaskHandler
The hardest part is how to dispatch different kind of IMessage to different handlers. Here is a useful link.
I really like this approach, it doesn't pollute my entity with service, and it doesn't have any God class.
SendEmailTask
seems more like a service than an entity to me. I would go for option 1 without and hesitation.accept
s visitors. The motivation for Visitor is that you have many class types in some aggregate that need visiting, and it's not convenient to modify their code for each new functionality (operation). I still don't see what those aggregate objects are, and think that Visitor is not appropriate. If it's the case, you should edit your question (which refers to visitor).