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 ?
Respostas:
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.
fonte
String
ouInt32
ou mesmoList
fora 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.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.
fonte
HttpContextBase
(que foi introduzida exatamente por esse motivo). Tenho certeza de que minha realidade seria realmente azeda sem essa aula.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.
fonte
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 naLogger
interface. Não foi possível começar a usar um novoLogger
de 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
Adapter
para 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 oAdapter
. E, com certeza, injete o logger real noAdapter
- mas não em todas as pequenas coisas que possam precisar dele.fonte
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.
fonte
O registro genuíno é um caso especial.
@Telastyn escreve:
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:
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.
fonte
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.
fonte
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
class
es 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.
fonte
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:
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
IConverter
fosse umSqlConnection
, eu ficaria bastante horrorizado ao ver isso como uma chamada estática. As razões pelas quais são falhas óbvias. Eu apontaria que aSQLConnection
pode ser razoavelmente "transversal" em uma aplicação, para que eu não tivesse usado exatamente essas palavras.O log é mais parecido com um
SQLConnection
ouConvert.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 seConvert
foi 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 parecidoLogger.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
_logger
para 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.
fonte
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.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.
fonte
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.
fonte