Aplicabilidade do princípio de responsabilidade única

40

Recentemente, deparei-me com um problema arquitetônico aparentemente trivial. Eu tinha um repositório simples no meu código que foi chamado assim (o código está em C #):

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges era um invólucro simples que confirma as alterações no banco de dados:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

Depois de algum tempo, eu precisei implementar uma nova lógica que enviaria notificações por email toda vez que um usuário fosse criado no sistema. Como havia muitas chamadas para _userRepository.Add()e SaveChangesao redor do sistema, decidi atualizar SaveChangesassim:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

Dessa forma, o código externo pode se inscrever no UserCreatedEvent e manipular a lógica comercial necessária para enviar notificações.

Mas foi apontado para mim que minha modificação SaveChangesviolou o princípio de Responsabilidade Única, e isso SaveChangesdeve salvar e não disparar nenhum evento.

Este é um ponto válido? Parece-me que a criação de um evento aqui é essencialmente a mesma coisa que o log: apenas adicionando alguma funcionalidade lateral à função. E o SRP não proíbe o uso de eventos de registro ou disparo em suas funções, apenas diz que essa lógica deve ser encapsulada em outras classes e não há problema em um repositório chamar essas outras classes.

Andre Borges
fonte
22
Sua resposta é: "OK, então como você a escreveria para que não viole o SRP, mas ainda permita um único ponto de modificação?"
Robert Harvey
43
Minha observação é que a realização de um evento não adiciona uma responsabilidade adicional. De fato, muito pelo contrário: ele delega a responsabilidade em outro lugar.
Robert Harvey
Acho que seu colega de trabalho está certo, mas sua pergunta é válida e útil, tão votada!
Andres F.
16
Não existe uma definição definitiva de uma única responsabilidade. A pessoa que indica que viola o SRP está correta usando sua definição pessoal e você está correto usando sua definição. Eu acho que seu design está perfeitamente bem com a ressalva de que este evento não é único, pelo qual outras funcionalidades semelhantes são feitas de maneiras diferentes. A consistência é muito, muito, muito mais importante de se prestar atenção do que algumas orientações vagas como a SRP, que levaram ao extremo, acabam com toneladas de aulas muito fáceis de entender que ninguém sabe como fazer funcionar em um sistema.
Dunk

Respostas:

14

Sim, pode ser um requisito válido ter um repositório que dispara determinados eventos em determinadas ações como Addou SaveChanges- e não vou questionar isso (como algumas outras respostas) apenas porque seu exemplo específico de adição de usuários e envio de e-mails pode parecer um um pouco artificial. A seguir, vamos supor que esse requisito seja perfeitamente justificado no contexto do seu sistema.

Então , sim , a codificação da mecânica de eventos, o registro e o salvamento em um método violam o SRP . Em muitos casos, é provavelmente uma violação aceitável, especialmente quando ninguém deseja distribuir as responsabilidades de manutenção de "salvar alterações" e "aumentar evento" para diferentes equipes / mantenedores. Mas vamos supor que um dia alguém queira fazer exatamente isso, isso pode ser resolvido de uma maneira simples, talvez colocando o código dessas preocupações em diferentes bibliotecas de classes?

A solução para isso é deixar o repositório original permanecer responsável por confirmar as alterações no banco de dados, nada mais, e criar um repositório proxy que tenha exatamente a mesma interface pública, reutilizar o repositório original e adicionar a mecânica de evento adicional aos métodos.

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

Você pode chamar a classe proxy como NotifyingRepositoryou, ObservableRepositoryse desejar, de acordo com a resposta altamente votada de @ Peter (que na verdade não diz como resolver a violação do SRP, apenas dizendo que a violação está correta).

A classe de repositório nova e antiga devem derivar de uma interface comum, como mostrado na descrição clássica do padrão Proxy .

Em seguida, no seu código original, inicialize _userRepositorypor um objeto da nova EventFiringUserRepoclasse. Dessa forma, você mantém o repositório original separado da mecânica do evento. Se necessário, você pode ter o repositório de acionamento de eventos e o repositório original lado a lado e deixar os chamadores decidirem se usam o primeiro ou o último.

Para abordar uma preocupação mencionada nos comentários: isso não leva a proxies em cima de proxies em cima de proxies, e assim por diante? Na verdade, adicionar a mecânica de eventos cria uma base para adicionar requisitos adicionais do tipo "enviar e-mails", basta se inscrever nos eventos, aderindo ao SRP com esses requisitos também, sem proxies adicionais. Mas a única coisa que deve ser adicionada uma vez aqui é a própria mecânica do evento.

Se esse tipo de separação realmente vale a pena no contexto do seu sistema, é algo que você e seu revisor precisam decidir por si mesmos. Provavelmente não separaria o log do código original, nem usando outro proxy, não adicionando um logger ao evento do ouvinte, embora isso fosse possível.

Doc Brown
fonte
3
Além desta resposta. Existem alternativas para proxies, como AOP .
Laiv 27/03
11
Acho que você não entendeu, não é que a criação de um evento interrompa o SRP, é que a criação de um evento apenas para usuários "Novos" exige que o repositório seja responsável por saber o que constitui um usuário "Novo" e não um "Recém-adicionado a mim" "usuário
Ewan
@ Ewan: leia a pergunta novamente. Tratava-se de um lugar no código que executa determinadas ações que precisam ser acopladas a outras ações fora da responsabilidade desse objeto. E colocar a ação e o levantamento de eventos em um só lugar foi questionado por um revisor por quebrar o SRP. O exemplo de "salvar novos usuários" é apenas para fins de demonstração; chame o exemplo de artificial, se quiser, mas esse não é o ponto da questão.
Doc Brown
2
Esta é a melhor / resposta correta IMO. Ele não apenas mantém o SRP, mas também mantém o princípio Aberto / Fechado. E pense em todos os testes automatizados que as alterações dentro da classe podem quebrar. Modificar os testes existentes quando novas funcionalidades são adicionadas é um grande cheiro.
Keith Payne
11
Como essa solução funciona se houver uma transação em andamento? Quando isso acontece, na SaveChanges()verdade não cria o registro do banco de dados e pode acabar sendo revertido. Parece que você precisaria substituir AcceptAllChangesou assinar o evento TransactionCompleted.
John Wu
29

Enviar uma notificação de que o armazenamento de dados persistente foi alterado parece uma coisa sensata a ser feita ao salvar.

É claro que você não deve tratar o Add como um caso especial - você também teria que acionar eventos para Modificar e Excluir. É o tratamento especial do caso "Adicionar" que cheira, força o leitor a explicar por que cheira e, finalmente, leva alguns leitores do código a concluir que ele deve violar o SRP.

Um repositório "de notificação" que pode ser consultado, alterado e dispara eventos em alterações é um objeto perfeitamente normal. Você pode esperar encontrar várias variações em praticamente qualquer projeto de tamanho decente.


Mas um repositório de "notificação" é realmente o que você precisa? Você mencionou o C #: muitas pessoas concordam que usar um em System.Collections.ObjectModel.ObservableCollection<>vez de System.Collections.Generic.List<>quando o último é tudo o que você precisa é de todos os tipos de coisas ruins e erradas, mas poucas apontam imediatamente para o SRP.

O que você está fazendo agora é trocar o seu UserList _userRepositorypor um ObservableUserCollection _userRepository. Se esse é o melhor curso de ação ou não, depende da aplicação. Mas, embora inquestionavelmente torne o _userRepositorypeso consideravelmente menor, na minha humilde opinião, não viola o SRP.

Pedro
fonte
O problema de usar ObservableCollectionpara este caso é que ele dispara o evento equivalente não na chamada para SaveChanges, mas na chamada para Add, o que levaria a um comportamento muito diferente daquele mostrado no exemplo. Veja minha resposta sobre como manter o repositório original leve e ainda manter o SRP mantendo a semântica intacta.
Doc Brown
@DocBrown Invoquei as classes conhecidas ObservableCollection<>e List<>para comparação e contexto. Não pretendia recomendar o uso de classes reais para a implementação interna ou a interface externa.
Peter
Ok, mas mesmo que o OP adicione eventos a "Modify" e "Delete" (que eu acho que o OP deixou para manter a pergunta concisa, por uma questão de simplicidade), acho que um revisor pode facilmente chegar à conclusão de uma violação SRP. Provavelmente é aceitável, mas nenhum que não possa ser resolvido se necessário.
Doc Brown
16

Sim, é uma violação do princípio de responsabilidade única e um ponto válido.

Um design melhor seria ter um processo separado para recuperar 'novos usuários' do repositório e enviar os emails. Acompanhar quais usuários receberam um email, falhas, reenvios etc., etc.

Dessa forma, você pode lidar com erros, falhas e similares, além de evitar que seu repositório atenda a todos os requisitos que tenham a ideia de que os eventos acontecem "quando algo está comprometido com o banco de dados".

O repositório não sabe que um usuário que você adiciona é um novo usuário. Sua responsabilidade é simplesmente armazenar o usuário.

Provavelmente vale a pena expandir nos comentários abaixo.

O repositório não sabe que um usuário adicionado é um novo usuário - sim, ele possui um método chamado Adicionar. Sua semântica implica que todos os usuários adicionados sejam novos. Combine todos os argumentos passados ​​para Adicionar antes de chamar Salvar - e você obtém todos os novos usuários

Incorreta. Você está confluindo "Adicionado ao repositório" e "Novo".

"Adicionado ao Repositório" significa exatamente o que diz. Posso adicionar, remover e adicionar novamente usuários a vários repositórios.

"Novo" é um estado de um usuário definido pelas regras de negócios.

Atualmente, a regra de negócios pode ser "Novo == acabado de ser adicionado ao repositório", mas isso não significa que não é uma responsabilidade separada conhecer e aplicar essa regra.

Você precisa ter cuidado para evitar esse tipo de pensamento centrado no banco de dados. Você terá processos de casos extremos que adicionam usuários não novos ao repositório e, quando enviar e-mails para eles, toda a empresa dirá "Claro que esses não são usuários 'novos'! A regra real é X"

Esta resposta está IMHO bastante errado: o repositório é exatamente o único lugar central no código que sabe quando novos usuários são adicionados

Incorreta. Pelas razões acima, além disso, não é um local central, a menos que você inclua o código de envio de email na classe em vez de apenas criar um evento.

Você terá aplicativos que usam a classe de repositório, mas não tem o código para enviar o email. Quando você adiciona usuários a esses aplicativos, o email não será enviado.

Ewan
fonte
11
O repositório não sabe que um usuário adicionado é um novo usuário - sim, ele possui um método chamado Add. Sua semântica implica que todos os usuários adicionados sejam novos. Combine todos os argumentos passados Addantes da chamada Save- e você obtém todos os novos usuários.
Andre Borges
Eu gosto dessa sugestão. No entanto, o pragmatismo prevalece sobre a pureza. Dependendo das circunstâncias, pode ser difícil justificar a adição de uma camada arquitetural totalmente nova a um aplicativo existente, se tudo o que você precisa fazer for literalmente enviar um único email quando um usuário for adicionado.
Alexander
Mas o evento não está dizendo que o usuário foi adicionado. Diz que o usuário foi criado. Se considerarmos nomear as coisas corretamente e concordarmos com as diferenças semânticas entre adicionar e criar, o evento no snippet terá um nome errado ou será extraviado. Eu não acho que o revisor tenha algo contra os repositórios notyfing. Provavelmente estava preocupado com o tipo de evento e seus efeitos colaterais.
Laiv 27/03
7
@ Andre Novo no repo, mas não necessariamente "novo" no sentido comercial. é a fusão dessas duas idéias que está escondendo a responsabilidade extra à primeira vista. Posso importar uma tonelada de usuários antigos para o meu novo repositório ou remover e adicionar novamente um usuário etc. Haverá regras de negócios sobre o que um "novo usuário" está além "foi adicionado ao dB"
Ewan
11
Nota do moderador: Sua resposta não é uma entrevista jornalística. Se você tiver edições, incorpore-as naturalmente à sua resposta sem criar todo o efeito "notícias de última hora". Não somos um fórum de discussão.
Robert Harvey
7

Este é um ponto válido?

Sim, apesar de depender muito da estrutura do seu código. Como não tenho o contexto completo, tentarei falar em geral.

Parece-me que a criação de um evento aqui é essencialmente a mesma coisa que o log: apenas adicionando alguma funcionalidade lateral à função.

Absolutamente não é. O log não faz parte do fluxo de negócios, pode ser desativado, não deve causar efeitos colaterais (comerciais) e não deve influenciar o estado e a integridade do seu aplicativo de maneira alguma, mesmo que por algum motivo você não tenha conseguido efetuar o log mais nada. Agora compare isso com a lógica que você adicionou.

E o SRP não proíbe o uso de eventos de registro ou disparo em suas funções, apenas diz que essa lógica deve ser encapsulada em outras classes e não há problema em um repositório chamar essas outras classes.

O SRP trabalha em conjunto com o ISP (S e I no SOLID). Você acaba com muitas classes e métodos que fazem coisas muito específicas e nada mais. Eles são muito focados, muito fáceis de atualizar ou substituir e, em geral, fáceis de testar. É claro que, na prática, você também terá algumas classes maiores que lidam com a orquestração: elas terão várias dependências e se concentrarão não em ações atomizadas, mas em ações de negócios, que podem exigir várias etapas. Desde que o contexto de negócios seja claro, eles também podem ser chamados de responsabilidade única, mas como você disse corretamente, à medida que o código cresce, convém abstrair parte dele em novas classes / interfaces.

Agora, de volta ao seu exemplo em particular. Se você absolutamente precisar enviar uma notificação sempre que um usuário for criado e talvez até executar outras ações mais especializadas, poderá criar um serviço separado que encapsule esse requisito, algo como UserCreationService, que expõe um método Add(user), que lida com o armazenamento (a chamada ao seu repositório) e a notificação como uma única ação comercial. Ou faça isso no seu snippet original, depois de_userRepository.SaveChanges();

assíncrono
fonte
2
O registro não faz parte do fluxo de negócios - como isso é relevante no contexto do SRP? Se o objetivo do meu evento fosse enviar novos dados do usuário para o Google Analytics - desativá-los teria o mesmo efeito comercial que desativar o log: não é crítico, mas é bastante perturbador. Qual é a regra geral para adicionar / não adicionar nova lógica a uma função? "Desativá-lo causará grandes efeitos colaterais nos negócios?"
Andre Borges
2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . E se você estiver disparando eventos prematuros, causando "notícias" falsas. E se as análises levarem em conta "usuários" que não foram finalmente criados devido a erros na transação do banco de dados? E se a empresa estiver tomando decisões sobre premissas falsas, apoiadas por dados imprecisos? Você está muito focado no lado técnico da questão. "Às vezes você não consegue ver a madeira das árvores"
Laiv
@ Laiv, você está fazendo uma observação válida, mas esse não é o ponto da minha pergunta ou esta resposta. A questão é se essa é uma solução válida no contexto do SRP, então vamos supor que não haja erros de transação do banco de dados.
Andre Borges
Você está basicamente me pedindo para lhe dizer o que você quer ouvir. Eu apenas lhe dou escopo. Um escopo mais amplo para você decidir se o SRP é importante ou não, porque o SRP é inútil sem o contexto apropriado. Na IMO, a maneira como você aborda a preocupação está incorreta porque você está focando apenas na solução técnica. Você deve dar relevância suficiente a todo o contexto. E sim, o DB pode falhar. Há uma chance de isso acontecer e você não deve omitir isso, porque, como você sabe, as coisas acontecem e essas coisas podem mudar de idéia em relação a dúvidas sobre SRP ou outras boas práticas.
Laiv 28/03
11
Dito isto, lembre-se de que princípios não são regras escritas em pedra. Eles são permeáveis ​​(adaptáveis). Como você pode ver, eles estão abertos à interpretação. Seu revisor tem uma interpretação e você tem outra. Tente ver o que vê, resolva suas dúvidas e preocupações ou deixe que ele resolva as suas. Você não encontrará a resposta "certa" aqui. A resposta correta depende de você e do revisor, solicitando primeiro os requisitos (funcionais e não funcionais) do projeto.
Laiv 28/03
4

O SRP é, teoricamente, sobre pessoas , como o tio Bob explica em seu artigo O princípio de responsabilidade única . Obrigado Robert Harvey por fornecê-lo em seu comentário.

A pergunta correta é:

Qual "parte interessada" adicionou o requisito "enviar emails"?

Se essa parte interessada também é responsável pela persistência dos dados (improvável, mas possível), isso não viola o SRP. Caso contrário, ele faz.

user949300
fonte
2
Interessante - nunca ouvi falar dessa interpretação do SRP. Você tem alguma indicação para mais informações / literatura sobre esta interpretação?
sleske 27/03
2
@sleske: do próprio tio Bob : "E isso chega ao cerne do Princípio da Responsabilidade Única. Esse princípio é sobre as pessoas. Quando você escreve um módulo de software, quer ter certeza de que, quando as alterações forem solicitadas, essas alterações só poderão se originar. de uma única pessoa, ou melhor, de um único grupo de pessoas fortemente unidas, representando uma única função comercial definida de maneira restrita ".
Robert Harvey
Obrigado Robert. OMI, O nome "Princípio da Responsabilidade Única" é terrível, pois parece simples, mas poucas pessoas seguem o significado pretendido de "responsabilidade". Assim como a OOP mudou de muitos de seus conceitos originais, e agora é um termo sem sentido.
user949300 27/03
11
Sim. Foi o que aconteceu com o termo REST. Até Roy Fielding diz que as pessoas estão usando errado.
Robert Harvey
Embora a citação esteja relacionada, acho que essa resposta esquece que o requisito "enviar e-mails" não é um dos requisitos diretos de que trata a questão de violação do SRP. No entanto, ao dizer "Qual" parte interessada "adicionou o requisito" evento de aumento "" , essa resposta se tornaria mais relacionada à pergunta real. Modifiquei minha própria resposta um pouco para deixar isso mais claro.
Doc Brown
2

Embora tecnicamente não haja nada de errado com os repositórios notificando eventos, sugiro analisá-lo do ponto de vista funcional, onde sua conveniência suscita algumas preocupações.

Criando um usuário, decidindo o que é um novo usuário e sua persistência são três coisas diferentes .

Premissa minha

Considere a premissa anterior antes de decidir se o repositório é o local adequado para notificar eventos de negócios (independentemente do SRP). Observe que eu disse evento de negócios porque para mim UserCreatedtem uma conotação diferente de UserStoredou UserAdded 1 . Eu também consideraria cada um desses eventos dirigidos a diferentes públicos.

Por um lado, criar usuários é uma regra específica de negócios que pode ou não envolver persistência. Pode envolver mais operações de negócios, envolvendo mais operações de banco de dados / rede. Operações das quais a camada de persistência desconhece. A camada de persistência não possui contexto suficiente para decidir se o caso de uso foi encerrado com êxito ou não.

Por outro lado, não é necessariamente verdade que _dataContext.SaveChanges();persistiu o usuário com sucesso. Depende do período de transação do banco de dados. Por exemplo, pode ser verdade para bancos de dados como o MongoDB, cujas transações são atômicas, mas não para os RDBMS tradicionais implementando transações ACID, nas quais poderia haver mais transações envolvidas e ainda a serem confirmadas.

Este é um ponto válido?

Poderia ser. No entanto, eu ousaria dizer que não é apenas uma questão de SRP (tecnicamente falando), é também uma questão de conveniência (funcionalmente falando).

  • É conveniente disparar eventos de negócios de componentes que desconhecem as operações de negócios em andamento?
  • Eles representam o lugar certo, tanto quanto o momento certo para fazê-lo?
  • Devo permitir que esses componentes orquestrem minha lógica de negócios por meio de notificações como esta?
  • Posso invalidar os efeitos colaterais causados ​​por eventos prematuros? 2

Parece-me que criar um evento aqui é essencialmente a mesma coisa que registrar

Absolutamente não. No entanto, o registro não tem efeitos colaterais, pois você sugeriu que o evento UserCreatedprovavelmente causará outras operações de negócios. Como notificações. 3

apenas diz que essa lógica deve ser encapsulada em outras classes e não há problema em um repositório chamar essas outras classes

Não é necessariamente verdade. O SRP não é apenas uma preocupação específica de classe. Opera em diferentes níveis de abstração, como camadas, bibliotecas e sistemas! Trata-se de coesão, de manter unido o que muda pelas mesmas razões, pelas mãos das mesmas partes interessadas . Se a criação do usuário ( caso de uso ) mudar, é provável que o momento e os motivos do evento também mudem.


1: Nomear as coisas adequadamente também é importante.

2: Digamos que enviamos UserCreateddepois _dataContext.SaveChanges();, mas toda a transação do banco de dados falhou posteriormente devido a problemas de conexão ou violações de restrições. Tenha cuidado com a transmissão prematura de eventos, porque seus efeitos colaterais podem ser difíceis de desfazer (se isso for possível).

3: Os processos de notificação não tratados adequadamente podem fazer com que você dispare notificações que não podem ser desfeitas / sup>

Laiv
fonte
11
+1 Ponto muito bom sobre o período de transação. Pode ser prematuro afirmar que o usuário foi criado, porque podem ocorrer reversões; e, diferentemente de um log, é provável que alguma outra parte do aplicativo faça algo com o evento.
Andres F.
2
Exatamente. Eventos denotam certeza. Algo aconteceu, mas acabou.
Laiv 26/03
11
@ Laiv: Exceto quando não o fazem. A Microsoft tem todos os tipos de eventos prefixados Beforeou Previewque não garantem a certeza.
Robert Harvey
11
@ jpmc26: Sem uma alternativa, sua sugestão não é útil.
Robert Harvey
11
@ jpmc26: Portanto, sua resposta é "mude para um ecossistema de desenvolvimento completamente diferente, com um conjunto totalmente diferente de ferramentas e características de desempenho". Me chame de contrário, mas imagino que isso seja inviável para a grande maioria dos esforços de desenvolvimento.
Robert Harvey
1

Não, isso não viola o SRP.

Muitos parecem pensar que o Princípio da Responsabilidade Única significa que uma função deve fazer apenas "uma coisa" e depois se envolver na discussão sobre o que constitui "uma coisa".

Mas não é isso que o princípio significa. Trata-se de preocupações em nível de negócios. Uma classe não deve implementar várias preocupações ou requisitos que possam mudar independentemente no nível de negócios. Digamos que uma classe armazene o usuário e envie uma mensagem de boas-vindas codificada por email. Várias preocupações independentes podem fazer com que os requisitos dessa classe sejam alterados. O designer pode exigir que o html / stylesheet do correio seja alterado. O especialista em comunicação pode exigir que o texto do e-mail seja alterado. E o especialista em UX pode decidir que o email deve realmente ser enviado em um ponto diferente no fluxo de integração. Portanto, a classe está sujeita a várias alterações de requisitos de fontes independentes. Isso viola o SRP.

Mas disparar um evento não viola o SRP, pois o evento depende apenas de salvar o usuário e não de qualquer outra preocupação. Os eventos são realmente uma maneira muito legal de manter o SRP, pois você pode ter um e-mail acionado pelo salvamento sem que o repositório seja afetado - ou até mesmo saiba sobre - o e-mail.

JacquesB
fonte
1

Não se preocupe com o princípio de responsabilidade única. Isso não ajudará você a tomar uma boa decisão aqui, porque você pode escolher subjetivamente um conceito específico como "responsabilidade". Você poderia dizer que a responsabilidade da classe é gerenciar a persistência de dados no banco de dados ou que a responsabilidade é executar todo o trabalho relacionado à criação de um usuário. Esses são apenas níveis diferentes do comportamento do aplicativo e são expressões conceituais válidas de uma "responsabilidade única". Portanto, este princípio é inútil para resolver seu problema.

O princípio mais útil a ser aplicado nesse caso é o princípio da menor surpresa . Então, vamos fazer a pergunta: é surpreendente que um repositório com a função principal de persistir dados em um banco de dados também envie e-mails?

Sim, é muito surpreendente. Esses são dois sistemas externos completamente separados, e o nome SaveChangesnão implica também no envio de notificações. O fato de você delegar isso a um evento torna o comportamento ainda mais surpreendente, pois alguém que lê o código não consegue mais ver facilmente quais comportamentos adicionais são chamados. O indireto prejudica a legibilidade. Às vezes, os benefícios valem os custos de legibilidade, mas não quando você invoca automaticamente um sistema externo adicional que tem efeitos observáveis ​​para os usuários finais. (O log pode ser excluído aqui, pois seu efeito é essencialmente a manutenção de registros para fins de depuração. Os usuários finais não consomem o log, portanto, não há nenhum dano em sempre o log.) Pior ainda, isso reduz a flexibilidade no tempo de enviar o email, tornando impossível intercalar outras operações entre o salvamento e a notificação.

Se seu código normalmente precisar enviar uma notificação quando um usuário for criado com sucesso, você poderá criar um método que faça isso:

public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
    repo.Add(user);
    repo.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Mas se isso agrega valor depende das especificidades do seu aplicativo.


Na verdade, eu desencorajaria a existência do SaveChangesmétodo. Esse método presumivelmente confirmará uma transação de banco de dados, mas outros repositórios podem ter modificado o banco de dados na mesma transação . O fato de confirmar todos eles é novamente surpreendente, pois SaveChangesestá especificamente vinculado a essa instância do repositório do usuário.

O padrão mais direto para gerenciar uma transação de banco de dados é um usingbloco externo :

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    context.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Isso dá ao programador controle explícito sobre quando as alterações em todos os repositórios são salvas, força o código a documentar explicitamente a sequência de eventos que devem ocorrer antes de uma confirmação, garante que uma reversão seja emitida por erro (supondo que isso ocorra DataContext.Dispose) e evita ocultos conexões entre classes com estado.

Também prefiro não enviar o email diretamente na solicitação. Seria mais robusto registrar a necessidade de uma notificação em uma fila. Isso permitiria um melhor tratamento de falhas. Em particular, se ocorrer um erro ao enviar o email, ele poderá ser tentado novamente mais tarde, sem interromper o salvamento do usuário, e evitará o caso em que o usuário foi criado, mas um erro é retornado pelo site.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

É melhor confirmar a fila de notificação primeiro, pois o consumidor da fila pode verificar se o usuário existe antes de enviar o email, caso a context.SaveChanges()chamada falhe. (Caso contrário, você precisará de uma estratégia de consolidação em duas fases completa para evitar erros de erro.)


A linha inferior é para ser prático. Na verdade, pense nas consequências (tanto em termos de risco quanto de benefício) de escrever código de uma maneira específica. Acho que o "princípio da responsabilidade única" não costuma me ajudar a fazer isso, enquanto o "princípio da menor surpresa" geralmente me ajuda a entrar na cabeça de outro desenvolvedor (por assim dizer) e pensar no que pode acontecer.

jpmc26
fonte
4
é surpreendente que um repositório com a função principal de persistir dados em um banco de dados também envie e-mails - acho que você não entendeu a questão. Meu repositório não está enviando e-mails. Apenas gera um evento, e como esse evento é tratado - é de responsabilidade do código externo.
Andre Borges
4
Você está basicamente argumentando "não use eventos".
Robert Harvey
3
[shrug] Os eventos são centrais para a maioria das estruturas da interface do usuário. Elimine eventos, e essas estruturas não funcionam.
Robert Harvey
2
@ jpmc26: Chama-se ASP.NET Webforms. Isso é péssimo.
Robert Harvey
2
My repository is not sending emails. It just raises an eventcausa efeito. O repositório está acionando o processo de notificação.
Laiv 28/03
0

Atualmente, SaveChangesfaz duas coisas: salva as alterações e os logs que faz. Agora você deseja adicionar outra coisa: envie notificações por email.

Você teve a ideia inteligente de adicionar um evento a ele, mas isso foi criticado por violar o Princípio de Responsabilidade Única (SRP), sem perceber que ele já havia sido violado.

Para obter uma solução SRP pura, primeiro ative o evento e chame todos os ganchos para esse evento, dos quais existem agora três: salvar, registrar e finalmente enviar emails.

Você aciona o evento primeiro ou precisa adicionar a SaveChanges. Sua solução é um híbrido entre os dois. Não aborda a violação existente, mas incentiva a prevenção de que ela ultrapasse três aspectos. Refatorar o código existente para estar em conformidade com o SRP pode exigir mais trabalho do que o estritamente necessário. Cabe ao seu projeto até que ponto eles querem levar o SRP.

CJ Dennis
fonte
-1

O código já violava o SRP - a mesma classe era responsável pela comunicação com o contexto e o log de dados.

Você acabou de atualizá-lo para ter 3 responsabilidades.

Uma maneira de retirar as coisas de uma responsabilidade seria abstrair a _userRepository; torná-lo um transmissor de comando.

Possui um conjunto de comandos, além de um conjunto de ouvintes. Ele recebe comandos e os transmite aos ouvintes. Possivelmente esses ouvintes estão ordenados e talvez eles possam até dizer que o comando falhou (que, por sua vez, é transmitido para ouvintes que já foram notificados).

Agora, a maioria dos comandos pode ter apenas 1 ouvinte (o contexto dos dados). SaveChanges, antes de suas alterações, possui 2 - o contexto dos dados e, em seguida, o criador de logs.

Sua alteração então adiciona outro ouvinte para salvar as alterações, que é gerar novos eventos criados pelo usuário no serviço de eventos.

Existem alguns benefícios nisso. Agora você pode remover, atualizar ou replicar o código de log sem que o restante do código seja importado. Você pode adicionar mais gatilhos nas alterações de salvamento para obter mais itens necessários.

Tudo isso é decidido quando o dispositivo _userRepositoryé criado e conectado (ou, talvez, esses recursos extras sejam adicionados / removidos em tempo real; poder adicionar / aprimorar o registro enquanto o aplicativo é executado).

Yakk
fonte