O objetivo da minha tarefa é projetar um pequeno sistema que possa executar tarefas recorrentes agendadas. Uma tarefa recorrente é algo como "envie um email para o administrador a cada hora, das 8:00 às 17:00, de segunda a sexta-feira".
Eu tenho uma classe base chamada 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(){
}
}
E eu tenho várias classes que são herdadas de RecurringTask . Um deles é chamado SendEmailTask .
public class SendEmailTask : RecurringTask{
private Email email;
public SendEmailTask(Email email){
this.email = email;
}
public override void Run(){
// need to send out email
}
}
E eu tenho um EmailService que pode me ajudar a enviar um email.
A última classe é RecurringTaskScheduler , é responsável por carregar tarefas do cache ou banco de dados e executar a tarefa.
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();
}
}
}
}
Aqui está o meu problema: onde devo colocar o EmailService ?
Opção 1 : Inject EmailService em 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);
}
}
Já existem algumas discussões sobre se devemos injetar um serviço em uma entidade e a maioria das pessoas concorda que não é uma boa prática. Veja este artigo .
Option2: If ... Else em 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
}
}
}
}
}
Foi-me dito se ... Else e elenco como acima não é OO, e trará mais problemas.
Opção3: altere a assinatura de Executar e crie ServiceBundle .
public class ServiceBundle{
public EmailService EmailService{get;set}
public CleanDiskService CleanDiskService{get;set;}
// and other services for other recurring tasks
}
Injete esta classe no 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);
}
}
}
}
O método Run de SendEmailTask seria
public void Run(ServiceBundle serviceBundle){
serviceBundle.EmailService.send(this.email);
}
Não vejo grandes problemas com essa abordagem.
Opção4 : padrão de visitante.
A idéia básica é criar um visitante que encapsule serviços como o 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){
//
}
}
E também precisamos alterar a assinatura do método Run . O método Run de SendEmailTask é
public void Run(RecurringTaskVisitor visitor){
visitor.visit(this);
}
É uma implementação típica do Padrão do Visitante, e o visitante será injetado no RecurringTaskScheduler .
Em resumo: dentre essas quatro abordagens, qual é a melhor para o meu cenário? E há alguma grande diferença entre a Opção3 e a Opção4 para esse problema?
Ou você tem uma idéia melhor sobre esse problema? Obrigado!
Atualização em 22/05/2015 : Acho que a resposta de Andy resume muito bem minha intenção; Se você ainda está confuso sobre o problema em si, sugiro ler o post dele primeiro.
Acabei de descobrir que meu problema é muito semelhante ao problema do Envio de Mensagens , o que leva à Option5.
Opção 5 : Converter meu problema em Envio de mensagens .
Há um mapeamento individual entre meu problema e o problema de Envio de Mensagens :
Despachante de mensagens : receba sub-classes IMessage e despache- IMessage para seus manipuladores correspondentes. → RecurringTaskScheduler
IMessage : Uma interface ou uma classe abstrata. → RecurringTask
MessageA : estende-se do IMessage , com algumas informações adicionais. → SendEmailTask
MessageB : Outra subclasse de IMessage . → CleanDiskTask
MessageAHandler : Quando receber MessageA , manipule-o → SendEmailTaskHandler, que contém EmailService, e enviará um email quando receber SendEmailTask
MessageBHandler : O mesmo que MessageAHandler , mas manipula MessageB . → CleanDiskTaskHandler
A parte mais difícil é como despachar diferentes tipos de IMessage para diferentes manipuladores. Aqui está um link útil .
Eu realmente gosto dessa abordagem, ela não polui minha entidade com serviço e não possui nenhuma classe de Deus .
SendEmailTask
parece mais um serviço do que uma entidade para mim. Eu iria para a opção 1 sem e hesitação.accept
é visitantes. A motivação para o Visitor é que você tem muitos tipos de classe em alguns agregados que precisam ser visitados, e não é conveniente modificar o código para cada nova funcionalidade (operação). Ainda não vejo o que são esses objetos agregados e acho que o Visitor não é apropriado. Se for esse o caso, você deve editar sua pergunta (que se refere ao visitante).Respostas:
Eu diria que a opção 1 é o melhor caminho a seguir. O motivo pelo qual você não deve descartá-lo é que não
SendEmailTask
é uma entidade. Uma entidade é um objeto relacionado à retenção de dados e estado. Sua classe tem muito pouco disso. De fato, não é uma entidade, mas possui uma entidade: o objeto que você está armazenando. Isso significa que não deve receber um serviço ou ter um método. Em vez disso, você deve ter serviços que levam entidades, como a sua . Então você já está seguindo a idéia de manter os serviços fora das entidades.Email
Email
#Send
EmailService
Como
SendEmailTask
não é uma entidade, é perfeitamente adequado injetar o email e o serviço nele, e isso deve ser feito através do construtor. Fazendo injeção de construtor, podemos ter certeza de queSendEmailTask
está sempre pronto para realizar seu trabalho.Agora vamos ver por que não fazer as outras opções (especificamente com relação ao SOLID ).
opção 2
Foi-lhe dito com razão que a ramificação desse tipo trará mais dores de cabeça no caminho. Vamos ver o porquê. Primeiro, os
if
tendem a se agrupar e crescer. Hoje, é uma tarefa enviar e-mails, amanhã, cada tipo diferente de classe precisa de um serviço ou outro comportamento diferente. Gerenciar essaif
declaração se torna um pesadelo. Como estamos ramificando no tipo (e, neste caso, no tipo explícito ), estamos subvertendo o sistema de tipos incorporado ao nosso idioma.A opção 2 não é de responsabilidade única (SRP), porque o reutilizável
RecurringTaskScheduler
agora precisa conhecer todos esses tipos diferentes de tarefas e todos os tipos de serviços e comportamentos de que eles podem precisar. Essa classe é muito mais difícil de reutilizar. Também não é aberto / fechado (OCP). Como ele precisa saber sobre esse tipo de tarefa ou sobre esse (ou esse tipo de serviço ou sobre esse), alterações díspares em tarefas ou serviços podem forçar alterações aqui. Adicionar uma nova tarefa? Adicionar um novo serviço? Alterar a maneira como o email é tratado? MudançaRecurringTaskScheduler
. Como o tipo de tarefa é importante, ele não adere à Substituição Liskov (LSP). Não pode apenas ter uma tarefa e ser feito. Ele precisa solicitar o tipo e, com base no tipo, faça isso ou aquilo. Em vez de encapsular as diferenças nas tarefas, estamos colocando tudo isso naRecurringTaskScheduler
.Opção 3
A opção 3 tem alguns grandes problemas. Mesmo no artigo ao qual você vincula , o autor desencoraja isso:
Você está criando um localizador de serviço com sua
ServiceBundle
classe. Nesse caso, ele não parece estático, mas ainda possui muitos dos problemas inerentes a um localizador de serviço. Suas dependências agora estão ocultas sob issoServiceBundle
. Se eu fornecer a seguinte API da minha nova e interessante tarefa:Quais são os serviços que estou usando? Quais serviços precisam ser ridicularizados em um teste? O que me impede de usar todos os serviços do sistema, apenas porque?
Se eu quiser usar o seu sistema de tarefas para executar algumas tarefas, agora dependerei de todos os serviços do seu sistema, mesmo que eu use apenas alguns ou mesmo nenhum.
O
ServiceBundle
SRP não é realmente porque ele precisa conhecer todos os serviços em seu sistema. Também não é OCP. A adição de novos serviços significa alterações noServiceBundle
, e alterações noServiceBundle
podem significar alterações díspares nas tarefas em outros lugares.ServiceBundle
não segrega sua interface (ISP). Ele possui uma interface abrangente de todos esses serviços e, por ser apenas um provedor desses serviços, poderíamos considerar sua interface para abranger as interfaces de todos os serviços que ele fornece. As tarefas não aderem mais à Dependency Inversion (DIP), porque suas dependências são ofuscadas por trás doServiceBundle
. Isso também não segue o Princípio do Menor Conhecimento (também conhecido como Lei de Demeter), porque as coisas sabem muito mais coisas do que precisam.Opção 4
Anteriormente, você tinha muitos objetos pequenos que eram capazes de operar independentemente. A opção 4 pega todos esses objetos e os esmaga em um único
Visitor
objeto. Esse objeto atua como um objeto divino em todas as suas tarefas. Reduz seusRecurringTask
objetos a sombras anêmicas que simplesmente chamam um visitante. Todo o comportamento se move para oVisitor
. Precisa mudar o comportamento? Precisa adicionar uma nova tarefa? MudançaVisitor
.A parte mais desafiadora é que, como todos os comportamentos diferentes estão todos em uma única classe, a alteração de alguns arrastamentos polimórficos ao longo de todo o outro comportamento. Por exemplo, queremos ter duas maneiras diferentes de enviar email (talvez eles usem servidores diferentes, talvez?). Como faríamos isso? Poderíamos criar uma
IVisitor
interface e implementar isso, potencialmente duplicando código, como#Visit(ClearDiskTask)
no nosso visitante original. Então, se surgir uma nova maneira de limpar um disco, precisamos implementar e duplicar novamente. Então queremos os dois tipos de mudanças. Implemente e duplique novamente. Esses dois comportamentos diferentes e díspares estão intrinsecamente ligados.Talvez, em vez disso, possamos apenas subclasse
Visitor
? Subclasse com novo comportamento de email, subclasse com novo comportamento de disco. Nenhuma duplicação até agora! Subclasse com ambos? Agora, um ou outro precisa ser duplicado (ou ambos, se essa for sua preferência).Vamos comparar com a opção 1: precisamos de um novo comportamento de email. Podemos criar um novo
RecurringTask
que executa o novo comportamento, injetar em suas dependências e adicioná-lo à coleção de tarefas noRecurringTaskScheduler
. Nem precisamos falar sobre a limpeza de discos, porque essa responsabilidade está em outro lugar. Também temos à disposição toda a gama de ferramentas OO. Poderíamos decorar essa tarefa com o log, por exemplo.A opção 1 fornecerá o mínimo de dor e é a maneira mais correta de lidar com essa situação.
fonte
SendEmailTask
banco de dados, essa configuração deve ser uma classe de configuração separada que também deve ser injetada no seuSendEmailTask
. Se você estiver gerando dados a partir do seuSendEmailTask
, deverá criar um objeto de lembrança para armazenar o estado e colocá-lo no seu banco de dados.EMailTaskDefinitions
eEmailService
emSendEmailTask
? Então, noRecurringTaskScheduler
, preciso injetar algo comoSendEmailTaskRepository
cuja responsabilidade é carregar definição e serviço e injetá-losSendEmailTask
. Mas eu argumentaria agora que éRecurringTaskScheduler
necessário conhecer o Repositório de todas as tarefasCleanDiskTaskRepository
. E eu preciso mudarRecurringTaskScheduler
cada vez que tenho uma nova tarefa (para adicionar repositório no Agendador).RecurringTaskScheduler
deve apenas estar ciente do conceito de repositório de tarefas generalizado e aRecurringTask
. Ao fazer isso, pode depender de abstrações. Os repositórios de tarefas podem ser injetados no construtor deRecurringTaskScheduler
. Então, os diferentes repositórios precisam ser conhecidos apenas ondeRecurringTaskScheduler
é instanciado (ou pode ser oculto em uma fábrica e chamado a partir daí). Porque depende apenas das abstrações,RecurringTaskScheduler
não precisa mudar a cada nova tarefa. Essa é a essência da inversão de dependência.Você já viu as bibliotecas existentes, por exemplo, quartzo de primavera ou lote de primavera (não sei o que mais se adequa às suas necessidades)?
Para sua pergunta:
Suponho que o problema é que você deseja manter alguns metadados da tarefa de maneira polimórfica, para que uma tarefa de email tenha endereços de email atribuídos, uma tarefa de log no nível do log e assim por diante. Você pode armazenar uma lista daqueles na memória ou no banco de dados, mas, para separar as preocupações, não deseja que a entidade seja poluída com o código de serviço.
Minha solução proposta:
Eu separaria a parte de execução e os dados da tarefa, para ter eg
TaskDefinition
e aTaskRunner
. O TaskDefinition tem uma referência a um TaskRunner ou a um factory que cria um (por exemplo, se alguma configuração for necessária, como o smtp-host). A fábrica é específica - só pode manipular seEMailTaskDefinition
retorna apenas instâncias deEMailTaskRunner
s. Dessa forma, é mais OO e muda com segurança - se você introduzir um novo tipo de tarefa, precisará introduzir uma nova fábrica específica (ou reutilizar uma), caso contrário, não poderá compilar.Dessa forma, você terminaria com uma dependência: camada de entidade -> camada de serviço e vice-versa, porque o Runner precisa de informações armazenadas na entidade e provavelmente deseja fazer uma atualização para seu estado no banco de dados.
Você pode quebrar o círculo usando uma fábrica genérica, que pega uma TaskDefinition e retorna um TaskRunner específico , mas isso exigiria muitos ifs. Você pode usar a reflexão para encontrar um corredor com o mesmo nome de sua definição, mas tenha cuidado, pois essa abordagem pode custar algum desempenho e levar a erros de tempo de execução.
PS Estou assumindo Java aqui. Eu acho que é semelhante no .net. O principal problema aqui é a dupla ligação.
Para o padrão de visitante
Eu acho que ele deveria ser usado para trocar um algoritmo por diferentes tipos de objetos de dados em tempo de execução, do que para propósitos puramente de dupla ligação. Por exemplo, se você possui diferentes tipos de seguros e diferentes tipos de cálculo, por exemplo, porque diferentes países exigem. Em seguida, você escolhe um método de cálculo específico e o aplica em vários seguros.
No seu caso, você escolheria uma estratégia de tarefa específica (por exemplo, email) e a aplicaria a todas as suas tarefas, o que está errado, porque nem todas são tarefas de email.
PS: Eu não testei, mas acho que sua opção 4 também não funcionará, porque é dupla ligação novamente.
fonte
Eu discordo completamente desse artigo. Os serviços (concretamente sua "API") são parte importante do domínio comercial e, como tal, existirão no modelo de domínio. E não há problema com entidades no domínio comercial que fazem referência a outra coisa no mesmo domínio comercial.
É uma regra de negócios. E para fazer isso, é necessário o serviço que envia e-mails. E a entidade que gerencia
When X
deve saber sobre esse serviço.Mas existem alguns problemas com a implementação. Deve ser transparente para o usuário da entidade que a entidade esteja usando um serviço. Portanto, adicionar o serviço no construtor não é uma coisa boa. Esse também é um problema quando você está desserializando a entidade do banco de dados, porque precisa definir os dados da entidade e as instâncias de serviços. A melhor solução que posso pensar é usar a injeção de propriedade depois que a entidade foi criada. Talvez forçando cada instância recém-criada de qualquer entidade a passar pelo método "inicializar" que injeta todas as entidades que a entidade precisa.
fonte
Essa é uma ótima pergunta e um problema interessante. Proponho que você use uma combinação de padrões de Cadeia de responsabilidade e expedição dupla (exemplos de padrões aqui ).
Primeiro vamos definir a hierarquia de tarefas. Observe que agora existem vários
run
métodos para implementar o Double Dispatch.Em seguida, vamos definir a
Service
hierarquia. UsaremosService
s para formar a Cadeia de Responsabilidade.A peça final é a
RecurringTaskScheduler
que orquestra o processo de carregamento e execução.Agora, aqui está o aplicativo de exemplo demonstrando o sistema.
Executando as saídas do aplicativo:
EmailService executando SendEmailTask com conteúdo 'aqui é o primeiro email'
EmailService executando SendEmailTask com conteúdo 'aqui é o segundo email'
ExecuteService executando ExecuteTask com conteúdo '/ root / python'
ExecuteService executando ExecuteTask com conteúdo '/ bin / cat'
EmailService executando SendEmailTask com conteúdo content 'aqui é o terceiro email'
ExecuteService executando ExecuteTask com conteúdo '/ bin / grep'
fonte