O Padrão do Visitante é válido neste cenário?


9

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 .


Você não codificou um idioma ou plataforma, mas eu recomendo pesquisar no cron . Sua plataforma pode ter uma biblioteca que funciona da mesma forma (por exemplo, jcron, que parece meio extinta). O agendamento de trabalhos e tarefas é em grande parte um problema resolvido: você já procurou outras opções antes de criar suas próprias? Havia razões para não usá-los?

@ Snowman Podemos mudar para uma biblioteca madura mais tarde. Tudo depende do meu gerente. A razão de eu postar esta pergunta é que quero encontrar uma maneira de resolver esse 'tipo' de problema. Já vi esse tipo de problema mais de uma vez e não consegui encontrar uma solução elegante. Então, eu estou querendo saber se eu fiz algo errado.
Sher10ck

É justo, sempre tento recomendar a reutilização de código, se possível.

11
SendEmailTaskparece mais um serviço do que uma entidade para mim. Eu iria para a opção 1 sem e hesitação.
Bart van Ingen Schenau

3
O que falta (para mim) para o Visitor é a estrutura de classe que 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).
Fuhrmanator

Respostas:


4

Eu diria que a opção 1 é o melhor caminho a seguir. O motivo pelo qual você não deve descartá-lo é que nãoSendEmailTask é 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.EmailEmail#SendEmailService

Como SendEmailTasknã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 que SendEmailTaskestá 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 iftendem 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 essa ifdeclaraçã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 RecurringTaskScheduleragora 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ça RecurringTaskScheduler. 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 na RecurringTaskScheduler.

Opção 3

A opção 3 tem alguns grandes problemas. Mesmo no artigo ao qual você vincula , o autor desencoraja isso:

  • Você ainda pode usar um localizador de serviço estático…
  • Evito o localizador de serviço quando posso, especialmente quando o localizador de serviço deve ser estático…

Você está criando um localizador de serviço com sua ServiceBundleclasse. 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 isso ServiceBundle. Se eu fornecer a seguinte API da minha nova e interessante tarefa:

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
    }
}

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 ServiceBundleSRP 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 no ServiceBundle, e alterações no ServiceBundlepodem significar alterações díspares nas tarefas em outros lugares. ServiceBundlenã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 do ServiceBundle. 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 Visitorobjeto. Esse objeto atua como um objeto divino em todas as suas tarefas. Reduz seus RecurringTaskobjetos a sombras anêmicas que simplesmente chamam um visitante. Todo o comportamento se move para o Visitor. Precisa mudar o comportamento? Precisa adicionar uma nova tarefa? Mudança Visitor.

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 IVisitorinterface 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 RecurringTaskque executa o novo comportamento, injetar em suas dependências e adicioná-lo à coleção de tarefas no RecurringTaskScheduler. 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.


Sua análise sobre Otion2,3,4 é fantástica! Isso realmente me ajuda muito. Mas para a Option1, eu diria que * SendEmailTask ​​* é uma entidade. Ele tem id, seu padrão recorrente e outras informações úteis que devem ser armazenadas em db. Acho que Andy resume bem minha intenção. Talvez um nome como * EMailTaskDefinitions * seja mais apropriado. não quero poluir minha entidade com meu código de serviço. Eufórico menciona algum problema se eu injetar um serviço na entidade. Também atualizo minha pergunta e incluo a Option5, que considero a melhor solução até agora.
101010 Sher10ck

@ Sher10ck Se estiver a retirar a configuração do seu SendEmailTaskbanco de dados, essa configuração deve ser uma classe de configuração separada que também deve ser injetada no seu SendEmailTask. Se você estiver gerando dados a partir do seu SendEmailTask, deverá criar um objeto de lembrança para armazenar o estado e colocá-lo no seu banco de dados.
Cbojar

Eu preciso puxar a configuração do db, então você está sugerindo a injeção de ambos EMailTaskDefinitionse EmailServiceem SendEmailTask? Então, no RecurringTaskScheduler, preciso injetar algo como SendEmailTaskRepositorycuja responsabilidade é carregar definição e serviço e injetá-los SendEmailTask. Mas eu argumentaria agora que é RecurringTaskSchedulernecessário conhecer o Repositório de todas as tarefas CleanDiskTaskRepository. E eu preciso mudar RecurringTaskSchedulercada vez que tenho uma nova tarefa (para adicionar repositório no Agendador).
211015 Sher10ck

@ Sher10ck O RecurringTaskSchedulerdeve apenas estar ciente do conceito de repositório de tarefas generalizado e a RecurringTask. Ao fazer isso, pode depender de abstrações. Os repositórios de tarefas podem ser injetados no construtor de RecurringTaskScheduler. Então, os diferentes repositórios precisam ser conhecidos apenas onde RecurringTaskScheduleré instanciado (ou pode ser oculto em uma fábrica e chamado a partir daí). Porque depende apenas das abstrações, RecurringTaskSchedulernão precisa mudar a cada nova tarefa. Essa é a essência da inversão de dependência.
Cbojar

3

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 TaskDefinitione a TaskRunner. 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 se EMailTaskDefinitionretorna apenas instâncias de EMailTaskRunners. 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.


Você resume muito bem minha intenção, thx! Eu gostaria de quebrar o círculo. Como deixar TaskDefiniton mantém uma referência ao TaskRunner ou factory tem o mesmo problema que a Option1. Trato a fábrica ou o TaskRunner como serviço. Se o TaskDefinition precisar contém uma referência a eles, você injeta o serviço no TaskDefinition ou usa algum método estático, que estou tentando evitar.
101010 Sher10ck

1

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.

Quando X envia um email para Y.

É uma regra de negócios. E para fazer isso, é necessário o serviço que envia e-mails. E a entidade que gerencia When Xdeve 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.


Que artigo você está se referindo e que não concorda? No entanto, ponto de vista interessante sobre o modelo de domínio. Provavelmente você pode ver dessa maneira, porém, as pessoas geralmente evitam misturar serviços em entidades, porque isso criará um acoplamento rígido muito em breve.
226 Andy

@ Andy O que Sher10ck fez referência em sua pergunta. E não vejo como isso criaria um acoplamento rígido. Qualquer código mal escrito pode introduzir um acoplamento rígido.
Euphoric

1

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 runmétodos para implementar o 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;
    }
}

Em seguida, vamos definir a Servicehierarquia. Usaremos Services para formar a Cadeia de Responsabilidade.

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);
        }
    }
}

A peça final é a RecurringTaskSchedulerque orquestra o processo de carregamento e execução.

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);
            }
        }
    }
}

Agora, aqui está o aplicativo de exemplo demonstrando o sistema.

public class App {

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

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'


Eu posso ter muita tarefa . Toda vez que adiciono uma nova tarefa , preciso alterar RecurringTask e também todas as suas subclasses, porque preciso adicionar uma nova função como a execução booleana pública abstrata (OtherService otherService) . Acho que Option4, o padrão de visitantes que também implementa o envio duplo tem o mesmo problema.
101010 Sher10ck

Bom ponto. Editei minha resposta para que os métodos run (service) sejam definidos no RecurringTask e retornem false por padrão. Dessa forma, quando você precisar adicionar outra classe de tarefas, não precisará tocar nas tarefas dos irmãos.
Iluwatar 23/05
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.