As chamadas “preocupações transversais” são uma desculpa válida para quebrar o SOLID / DI / IoC?


43

Meus colegas gostam de dizer "log / cache / etc. É uma preocupação transversal" e, em seguida, continuam usando o singleton correspondente em qualquer lugar. No entanto, eles amam IoC e DI.

É realmente uma desculpa válida para quebrar o princípio SOLI D ?


27
Descobri que os princípios do SOLID são principalmente úteis apenas para aqueles que já têm a experiência necessária para segui-los sem saber de qualquer maneira.
Svidgen

1
Eu achei o termo "preocupação transversal" um termo bastante específico (vinculado a aspectos) e não necessariamente ser sinônimo de singleton. O registro pode ser uma preocupação transversal no sentido em que às vezes você deseja registrar uma mensagem genérica em vários locais da mesma maneira (por exemplo, registre todas as chamadas em um método de serviço com quem fez a chamada ou em outro tipo de registro de auditoria). No entanto, eu argumentaria que a exploração madeireira (no sentido geral) não é uma preocupação transversal apenas porque é usada em qualquer lugar. Eu acho que isso é mais uma "preocupação onipresente", embora esse não seja realmente um termo padrão.
Pace

4
@svidgen, os princípios do SOLID também são úteis quando aqueles que não os conhecem revisam seu código e perguntam por que você fez dessa maneira. É bom ser capaz de ponto em um princípio e dizer, "por isso"
candied_orange

1
Você tem algum exemplo de por que você acha que o log é um caso especial? É um canal bastante simplista para redirecionar alguns dados, geralmente parte de qualquer estrutura X que você esteja usando.
ksiimson

Respostas:


42

Não.

O SOLID existe como diretrizes para explicar mudanças inevitáveis. Você realmente nunca vai mudar sua biblioteca de log, destino, filtragem, formatação ou ...? Você realmente não vai mudar sua biblioteca de cache, destino, estratégia, escopo ou ...?

Claro que você é. No mínimo , você vai querer zombar dessas coisas de maneira sensata para isolá-las para testes. E se você quiser isolá-los para testes, provavelmente encontrará um motivo comercial em que deseja isolá-los por motivos da vida real.

E você terá o argumento de que o próprio criador de logs lidará com a mudança. "Ah, se o alvo / filtragem / formatação / estratégia mudar, apenas mudaremos a configuração!" Isso é lixo. Agora, você não apenas possui um Deus God que lida com todas essas coisas, mas também escreve seu código em XML (ou similar), onde não obtém análise estática, não obtém erros de tempo de compilação e não ' Realmente não realize testes de unidade eficazes.

Existem casos para violar as diretrizes do SOLID? Absolutamente. Às vezes, as coisas não mudam (sem exigir uma reescrita completa). Às vezes, uma leve violação do LSP é a solução mais limpa. Às vezes, criar uma interface isolada não fornece valor.

Mas o log e o cache (e outras preocupações transversais onipresentes) não são esses casos. Eles geralmente são ótimos exemplos dos problemas de acoplamento e design que você obtém quando ignora as diretrizes.


22
Que alternativa você tem então? Injetando um ILogger em todas as aulas que você escreve? Escrever um decorador para todas as turmas que precisam de um logger?
Robert Harvey

16
Sim. Se algo é uma dependência, faça disso uma dependência . E se isso significa muitas dependências, ou você está passando um logger em algum lugar que não deve funcionar - bom , agora você pode corrigir seu design.
Telastyn


20
O problema real que tenho com a IoC dogmática é que praticamente todo código moderno depende de algo que não é injetado nem abstraído. Se você escrever C #, por exemplo, você não está indo frequentemente abstrato Stringou Int32ou mesmo Listfora de seu módulo. Há apenas uma medida em que é razoável e sensato planejar mudanças. E, além dos tipos "centrais", na maioria das vezes óbvios, discernir o que você provavelmente pode mudar é realmente apenas uma questão de experiência e julgamento.
Svidgen

6
@svidgen - Espero que você não tenha lido minha resposta como defensora da IoC dogmática. E só porque não abstraímos esses tipos principais (e outras coisas onde prudente) não significa que não há problema em ter uma lista / string / int / etc global.
Telastyn

38

sim

Esse é o objetivo principal do termo "preocupação transversal" - significa algo que não se encaixa perfeitamente no princípio do SOLID.

É aqui que o idealismo se encontra com a realidade.

Pessoas semi-novas no SOLID e transversais enfrentam esse desafio mental. Tudo bem, não surte. Esforce-se para colocar tudo em termos do SOLID, mas há alguns lugares, como registro e cache, em que o SOLID simplesmente não faz sentido. O corte transversal é o irmão do SOLID, eles andam de mãos dadas.


9
Uma resposta fundamentada, prática e não dogmática.
User1936

11
Você pode nos dizer por que todos os cinco princípios do SOLID são quebrados por preocupações transversais? Eu tenho problemas para entender isso.
Doc Brown

4
Sim. Eu poderia pensar em uma boa razão para "quebrar" cada princípio individualmente; mas nem uma única razão para quebrar todos os 5!
Svidgen

1
Seria ideal para mim não ter que bater com a cabeça na parede toda vez que preciso zombar de um cache / registrador / cache singleton para um teste de unidade. Imagine como seria testar um aplicativo da Web sem a necessidade de unidade HttpContextBase(que foi introduzida exatamente por esse motivo). Tenho certeza de que minha realidade seria realmente azeda sem essa aula.
Devnull 13/05/19

2
@devnull talvez esse é o problema, então, em vez das próprias preocupações transversais ...
Boris a aranha

25

Para o registro, acho que é. O registro é generalizado e geralmente não está relacionado à funcionalidade do serviço. É comum e bem entendido usar os padrões singleton da estrutura de log. Caso contrário, você está criando e injetando loggers em todos os lugares e não deseja isso.

Uma questão acima é que alguém dirá 'mas como posso testar o log?' . Penso que normalmente não testo o log, além de afirmar que posso realmente ler os arquivos de log e entendê-los. Quando vi o log testado, geralmente é porque alguém precisa afirmar que uma classe realmente fez alguma coisa e está usando as mensagens de log para obter esse feedback. Prefiro registrar algum ouvinte / observador nessa classe e afirmar nos meus testes que isso é chamado. Você pode colocar o log de eventos nesse observador.

Eu acho que o cache é um cenário completamente diferente, no entanto.


3
Depois de me interessar um pouco pelo FP, comecei a desenvolver um grande interesse em usar a composição de funções (talvez monádicas) para incorporar coisas como registro, manipulação de erros etc. nos seus casos de uso sem colocar nada disso na lógica de negócios principal ( consulte fsharpforfunandprofit.com/rop )
sara

5
O registro não deve ser generalizado. Se for, você está registrando demais e está criando um ruído para si mesmo quando realmente precisa ver esses logs. Ao injetar a dependência do criador de logs, você é forçado a pensar se realmente precisa ou não registrar algo.
RubberDuck

12
@RubberDuck - Diga-me que "o log não deve ser difundido" quando recebo um relatório de erro do campo e a única capacidade de depuração que tenho para descobrir o que aconteceu é o arquivo de log que eu não queria difundir. Lição aprendida, o registro deve ser "difundido", muito difundido.
Dunk

15
@RubberDuck: Com o software de servidor, o registro é a única maneira de sobreviver. É a única maneira de descobrir um bug que ocorreu 12 horas atrás, em vez de agora em seu próprio laptop. Não se preocupe com barulho. Use o software de gerenciamento de log assim você pode consultar o seu log (idealmente você também deve configurar alarmes que lhe enviaremos em logs de erros)
slebetman

6
Nossa equipe de suporte me liga e diz "o cliente estava clicando em algumas páginas e ocorreu um erro". Pergunto a eles o que o erro disse, eles não sabem. Pergunto o que especificamente o cliente clicou, eles não sabem. Eu pergunto se eles podem se reproduzir, eles não podem. Concordo que o log pode ser alcançado principalmente com loggers bem implantados em alguns pontos importantes, mas as pequenas mensagens estranhas aqui e ali também são inestimáveis. O medo do registro leva a um software de menor qualidade. Se você acabar com muitos dados, corte novamente. Não otimize prematuramente.
Pace

12

Meus 2 centavos ...

Sim e não.

Você nunca deve realmente violar os princípios que adota; mas seus princípios sempre devem ser sutis e adotados em serviço a uma meta mais alta. Portanto, com um entendimento adequadamente condicionado, algumas violações aparentes podem não ser violações reais do "espírito" ou "corpo de princípios como um todo".

Os princípios do SOLID, em particular, além de exigir muitas nuances, acabam subservientes ao objetivo de "fornecer software funcional e sustentável". Portanto, aderir a qualquer princípio específico do SOLID é autodestrutivo e contraditório ao fazer isso em conflito com os objetivos do SOLID. E aqui, muitas vezes, observo que entregar a manutenção de trunfos .

Então, e o D no SOLID ? Bem, isso contribui para o aumento da capacidade de manutenção, tornando seu módulo reutilizável relativamente independente do seu contexto. E podemos definir o "módulo reutilizável" como "uma coleção de códigos que você planeja usar em outro contexto distinto". E isso se aplica a uma única função, classe, conjunto de classes e programas.

E sim, alterar as implementações do criador de logs provavelmente coloca seu módulo em "outro contexto distinto".

Então, deixe-me oferecer minhas duas grandes advertências :

Primeiro: desenhar as linhas em torno dos blocos de código que constituem "um módulo reutilizável" é uma questão de julgamento profissional. E seu julgamento é necessariamente limitado à sua experiência.

Se atualmente você não planeja usar um módulo em outro contexto, provavelmente não há problema em depender indefesamente dele. A ressalva a ressalva: Seus planos são provavelmente errado - mas isso é também OK. Quanto mais você escrever módulo após módulo, mais intuitivo e preciso será o seu senso de se "eu vou precisar disso novamente algum dia". Mas você provavelmente nunca será capaz de dizer retrospectivamente: "Modifiquei e desacoperei tudo da melhor maneira possível, mas sem excesso ".

Se você se sentir culpado por seus erros de julgamento, vá para a confissão e siga em frente ...

Em segundo lugar: inverter o controle não equivale a injetar dependências .

Isso é especialmente verdade quando você começa a injetar dependências ad nauseam . A injeção de dependência é uma tática útil para a estratégia abrangente de IoC. Mas eu argumentaria que o DI é de menor eficácia do que algumas outras táticas - como o uso de interfaces e adaptadores - pontos únicos de exposição ao contexto a partir do módulo.

E vamos realmente focar nisso por um segundo. Porque, mesmo se você injetar um Logger anúncio nauseam , precisará escrever um código na Loggerinterface. Não foi possível começar a usar um novo Loggerde um fornecedor diferente que aceita parâmetros em uma ordem diferente. Essa capacidade vem da codificação, dentro do módulo, de uma interface que existe dentro do módulo e que possui um único submódulo (adaptador) para gerenciar a dependência.

E se você estiver codificando em um Adaptador, se ele Loggeré injetado ou descoberto pelo Adaptador é geralmente bastante insignificante para o objetivo geral de manutenção. E o mais importante, se você possui um adaptador no nível do módulo, provavelmente é um absurdo injetá-lo em qualquer coisa. Está escrito para o módulo.

tl; dr - Pare de se preocupar com princípios sem considerar o motivo pelo qual você está usando os princípios. E, mais praticamente, basta criar um Adapterpara cada módulo. Use seu julgamento ao decidir onde você desenha os limites do "módulo". De dentro de cada módulo, vá em frente e consulte diretamente o Adapter. E, com certeza, injete o logger real no Adapter- mas não em todas as pequenas coisas que possam precisar dele.


4
Cara ... eu teria dado uma resposta mais curta, mas não tinha tempo.
Svidgen

2
+1 para usar um adaptador. Você precisa isolar dependências de componentes de terceiros para permitir que sejam substituídos sempre que possível. O log é um destino fácil para isso - existem muitas implementações de log e a maioria possui APIs semelhantes, portanto, uma implementação simples do adaptador permite que você as altere com muita facilidade. E para as pessoas que dizem que você nunca mudará seu provedor de log: eu tive que fazer isso, e isso causou muita dor porque eu não estava usando um adaptador.
Jules

"Os princípios do SOLID, em particular, além de exigir muitas nuances, são subservientes ao objetivo de" fornecer software funcional e sustentável "." - Essa é uma das melhores declarações que já vi. Como codificador de TOC leve, tive minha parcela de lutas com idealismo versus produtividade.
DVK

Toda vez que vejo um +1 ou um comentário em uma resposta antiga, leio minha resposta novamente e descubro, novamente, que sou uma escritora muito desorganizada e pouco clara ... Mas agradeço o comentário, @DVK.
svidgen

Os adaptadores economizam vidas, não custam muito e facilitam muito a sua vida, especialmente quando você deseja aplicar o SOLID. Testar é fácil com eles.
Alan Alan

8

A idéia de que o log deve sempre ser implementado como um singleton é uma daquelas mentiras que foram contadas tantas vezes que ganharam força.

Desde que os sistemas operacionais modernos funcionem, foi reconhecido que você pode desejar registrar-se em vários locais, dependendo da natureza da saída .

Os projetistas de sistemas devem constantemente questionar a eficácia das soluções anteriores antes de incluí-las cegamente nas novas. Se eles não estão realizando essa diligência, não estão fazendo seu trabalho.


2
Qual é a sua solução proposta, então? Ele está injetando um ILogger em todas as aulas que você escreve? Que tal usar um padrão Decorator?
Robert Harvey

4
Não estou realmente respondendo minha pergunta agora, é? Afirmei os padrões apenas como exemplos ... Não precisa ser assim se você tem algo melhor em mente.
Robert Harvey

8
Eu realmente não entendo esta resposta, em termos de propostas concretas
Brian Agnew

3
@RobbieDee - Então você está dizendo que o Log deve ser implementado da maneira mais complicada e inconveniente pelo "azar" de que podemos querer logar em vários locais? Mesmo que uma mudança como essa tenha ocorrido, você realmente acha que adicionar essa funcionalidade à instância existente do Logger será mais trabalho do que todo o esforço para passar esse Logger entre classes e alterar interfaces quando você decidir que deseja ou não um Logger as dezenas de projetos em que esse registro de vários locais nunca ocorrerá?
Dunk

2
Re: "você pode desejar fazer logon em vários locais, dependendo da natureza da saída": Claro, mas a melhor maneira de lidar com isso está na estrutura de log , em vez de tentar injetar várias dependências de log separadas. (Os quadros de registro comuns já suporta isto.)
ruach

4

O registro genuíno é um caso especial.

@Telastyn escreve:

Você realmente nunca vai mudar sua biblioteca de logs, destino, filtragem, formatação ou ...?

Se você antecipar que pode precisar alterar sua biblioteca de criação de log, deverá usar uma fachada; ou seja, SLF4J se você estiver no mundo Java.

Quanto ao resto, uma biblioteca de criação de log decente se encarrega de alterar para onde vai o log, quais eventos são filtrados, como os eventos de log são formatados usando arquivos de configuração do criador de logs e (se necessário) classes de plug-in personalizadas. Existem várias alternativas disponíveis no mercado.

Em resumo, esses são problemas resolvidos ... para o registro ... e, portanto, não há necessidade de usar a Injeção de Dependência para resolvê-los.

O único caso em que a DI pode ser benéfica (sobre as abordagens de registro padrão) é se você deseja submeter o registro do aplicativo ao teste de unidade. No entanto, suspeito que a maioria dos desenvolvedores diria que o log não faz parte da funcionalidade de uma classe e não é algo que precisa ser testado.

@Telastyn escreve:

E você terá o argumento de que o próprio criador de logs lidará com a mudança. "Ah, se o alvo / filtragem / formatação / estratégia mudar, apenas mudaremos a configuração!" Isso é lixo. Agora, você não apenas possui um Deus God que lida com todas essas coisas, mas também escreve seu código em XML (ou similar), onde não obtém análise estática, não obtém erros de tempo de compilação e não ' Realmente não realize testes de unidade eficazes.

Receio que seja uma reação muito teórica. Na prática, a maioria dos desenvolvedores e integradores de sistemas gosta do fato de que você pode configurar o log por meio de um arquivo de configuração. E eles gostam do fato de que não é esperado que eles testem unitariamente o log de um módulo.

Claro, se você encher as configurações de log, poderá obter problemas, mas eles se manifestarão como o aplicativo falhando durante a inicialização ou muito / pouco registro. 1) Esses problemas são facilmente corrigidos, corrigindo o erro no arquivo de configuração. 2) A alternativa é um ciclo completo de construção / análise / teste / implantação toda vez que você faz uma alteração nos níveis de registro. Isso não é aceitável.


3

Sim e não !

Sim: acho razoável que diferentes subsistemas (ou camadas semânticas, bibliotecas ou outras noções de pacote modular) aceitem (o mesmo ou) logger potencialmente diferente durante sua inicialização, em vez de todos os subsistemas que contam com o mesmo singleton compartilhado comum .

Contudo,

Não: ao mesmo tempo não é razoável parametrizar o log em cada pequeno objeto (por construtor ou método de instância). Para evitar inchaço desnecessário e sem sentido, as entidades menores devem usar o registrador singleton de seu contexto em anexo.


Essa é uma das razões entre muitas outras para pensar em modularidade nos níveis: os métodos são agrupados em classes, enquanto as classes são agrupadas em subsistemas e / ou camadas semânticas. Esses pacotes maiores são ferramentas valiosas de abstração; devemos considerar diferentes dentro dos limites modulares do que quando cruzá-los.


3

Primeiro, ele começa com um cache singleton forte; as próximas coisas que você vê são singletons fortes para a camada de banco de dados, que apresentam estado global, APIs não descritivas de classes e código não testável.

Se você decidir não ter um singleton para um banco de dados, provavelmente não é uma boa ideia ter um singleton para um cache, afinal eles representam um conceito muito semelhante, armazenamento de dados, usando apenas mecanismos diferentes.

O uso de um singleton em uma classe transforma uma classe com uma quantidade específica de dependências em uma classe que, teoricamente , possui um número infinito delas, porque você nunca sabe o que realmente está oculto por trás do método estático.

Na década passada, passei a programação, houve apenas um caso em que testemunhei um esforço para alterar a lógica de log (que foi escrita como singleton). Portanto, embora eu goste de injeções por dependência, o registro não é realmente uma preocupação tão grande. Cache, por outro lado, eu definitivamente sempre faria como uma dependência.


Sim, o registro raramente muda e geralmente não precisa ser um módulo trocável. No entanto, uma vez tentei testar uma unidade de classe auxiliar de log, que dependia estática do sistema de log. Decidi que o mecanismo mais fácil para torná-lo testável seria executar a classe em teste em um processo separado, configurar seu logger para gravar em STDOUT e analisar essa saída no meu caso de teste. obviamente, nunca quero nada além do tempo real, certo? Exceto quando testando fuso horário / DST casos extremos, é claro ...
amon

@amon: Os relógios são como fazer login, já que existe outro mecanismo que serve ao mesmo objetivo que o DI, ou seja, o Joda-Time e suas diversas portas. (Não que haja algo de errado com o uso de DI vez;. Mas é mais simples de usar Joda-Time diretamente do que tentar escrever um adaptador injectible costume, e eu nunca vi ninguém pesar fazê-lo)
ruach

@amon "Eu tive uma experiência semelhante com relógios em que você obviamente nunca desejaria nada além do tempo real, certo? Exceto ao testar casos de borda de fuso horário / horário de verão, é claro ..." - ou quando você percebe que um erro ocorreu corrompeu seu banco de dados e a única esperança de recuperá-lo é analisar o log de eventos e reproduzi-lo a partir do último backup ... mas, de repente, você precisa que todo o seu código funcione com base no carimbo de data e hora da entrada de registro atual e não no atual Tempo.
Jules

3

Sim e Não, mas principalmente Não

Presumo que a maior parte da conversa seja baseada em estática vs instância injetada. Ninguém está propondo que o log interrompa o SRP, presumo? Estamos falando principalmente do "princípio de inversão de dependência". Tbh, eu concordo principalmente com a falta de resposta de Telastyn.

Quando é bom usar estática? Porque claramente às vezes é bom. As respostas sim apontam para os benefícios da abstração e as respostas "não" indicam que elas são algo pelo qual você paga. Uma das razões pelas quais seu trabalho é difícil é que não há uma resposta que você possa anotar e aplicar a todas as situações.

Toma: Convert.ToInt32("1")

Eu prefiro isso:

private readonly IConverter _converter;

public MyClass(IConverter converter)
{
   Guard.NotNull(converter)
   _converter = conveter
}

.... 
var foo = _converter.ToInt32("1");

Por quê? Estou aceitando que precisarei refatorar o código se precisar de flexibilidade para trocar o código de conversão. Estou aceitando que não vou conseguir zombar disso. Acredito que a simplicidade e a concisão valem esse comércio.

Olhando para o outro extremo do espectro, se IConverterfosse um SqlConnection, eu ficaria bastante horrorizado ao ver isso como uma chamada estática. As razões pelas quais são falhas óbvias. Eu apontaria que a SQLConnectionpode ser razoavelmente "transversal" em uma aplicação, para que eu não tivesse usado exatamente essas palavras.

O log é mais parecido com um SQLConnectionou Convert.ToInt32? Eu diria mais como 'SQLConnection`.

Você deve estar zombando do log . Ele fala com o mundo exterior. Ao escrever um método usando Convert.ToIn32, estou usando-o como uma ferramenta para calcular outra saída separadamente testável da classe. Não preciso verificar se Convertfoi chamado corretamente ao verificar se "1" + "2" == "3". O registro é diferente, é uma saída totalmente independente da classe. Estou assumindo que é uma saída que tem valor para você, a equipe de suporte e os negócios. Sua turma não está funcionando se o registro não estiver correto, portanto os testes de unidade não devem passar. Você deve testar o que sua classe registra. Eu acho que esse é o argumento matador, eu realmente poderia parar por aqui.

Eu também acho que é algo que provavelmente mudará. Um bom registro não apenas imprime seqüências de caracteres, é uma visão do que seu aplicativo está fazendo (sou um grande fã do registro baseado em eventos). Vi o registro básico se transformar em UIs de relatórios bastante elaboradas. Obviamente, é muito mais fácil seguir nessa direção se o seu registro for _logger.Log(new ApplicationStartingEvent())e menos parecido Logger.Log("Application has started"). Pode-se argumentar que isso está criando inventário para um futuro que nunca pode acontecer, é uma decisão judicial e acho que vale a pena.

De fato, em um projeto pessoal meu, criei uma interface do usuário sem registro usando apenas o _loggerpara descobrir o que o aplicativo estava fazendo. Isso significava que eu não precisava escrever código para descobrir o que o aplicativo estava fazendo, e acabei com um registro sólido. Sinto que, se minha atitude em relação ao registro fosse simples e imutável, essa ideia não teria me ocorrido.

Então, eu concordo com Telastyn no caso de extração de madeira.


É assim que eu registro incidentalmente . Gostaria de vincular a um artigo ou algo assim, mas não consigo encontrar um. Se você está no mundo .NET e procura o log de eventos, encontrará Semantic Logging Application Block. Não o use, como a maioria do código criado pela equipe de "padrões e práticas" do MS, ironicamente, é um antipadrão.
Nathan Cooper

3

Primeiro, as preocupações transversais não são os principais componentes e não devem ser tratadas como dependências em um sistema. Um sistema deve funcionar se, por exemplo, o Logger não for inicializado ou o cache não estiver funcionando. Como você tornará o sistema menos acoplado e coeso? É aí que o SOLID entra em cena no design do sistema OO.

Manter o objeto como singleton não tem nada a ver com o SOLID. Esse é o ciclo de vida do seu objeto, por quanto tempo você deseja que ele fique na memória.

Uma classe que precisa de uma dependência para inicializar não deve saber se a instância de classe fornecida é única ou transitória. Mas tldr; se você estiver escrevendo Logger.Instance.Log () em todos os métodos ou classes, então o código problemático (cheiro de código / acoplamento rígido) é realmente muito complicado. Este é o momento em que as pessoas começam a abusar do SOLID. E outros desenvolvedores como o OP começam a fazer perguntas genuínas como essa.


2

Eu resolvi esse problema usando uma combinação de herança e características (também chamadas de mixins em alguns idiomas). Os traços são super úteis para resolver esse tipo de preocupação transversal. Geralmente é um recurso de idioma, então eu acho que a resposta real é que depende dos recursos de idioma.


Interessante. Nunca fiz nenhum trabalho significativo usando uma linguagem que suporte características, mas considerei usá-las para alguns projetos futuros, portanto, seria útil se você pudesse expandir isso e mostrar como as características resolvem o problema.
Jules
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.