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

43

Meus colegas gostam de dizer "registro / armazenamento em cache / etc. é uma preocupação transversal" e, em seguida, continuar usando o singleton correspondente em todos os lugares. No entanto, eles adoram IoC e DI.

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

    
por Den 13.05.2016 / 13:22
fonte

11 respostas

42

Não.

SOLID existe como diretrizes para explicar a mudança inevitável. Você realmente nunca irá mudar sua biblioteca de registro, ou alvo, ou filtragem, ou formatação, ou ...? Você realmente não vai mudar sua biblioteca, alvo, estratégia, escopo ou ...?

Claro que você é. Pelo menos , você vai querer zombar dessas coisas de uma maneira sã para isolá-las para testes. E se você quiser isolá-los para testes, é provável que você encontre uma razão comercial em que queira isolá-los por motivos reais.

E você receberá o argumento de que o próprio logger lidará com a mudança. "Ah, se o alvo / filtragem / formatação / estratégia mudar, então vamos apenas mudar a configuração!" Isso é lixo. Não apenas você agora tem um Objeto de Deus que lida com todas essas coisas, você está escrevendo seu código em XML (ou similar) onde você não obtém análise estática, você não obtém erros de tempo de compilação, e você não Realmente obtemos testes de unidade eficazes.

Existem casos que violam as diretrizes do SOLID? Absolutamente. Às vezes as coisas não mudam (sem exigir uma reescrita completa de qualquer maneira). Às vezes, uma leve violação do LSP é a solução mais limpa. Às vezes, fazer uma interface isolada não fornece nenhum valor.

Mas o registro em log e o armazenamento em cache (e outras questõ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.

    
por 13.05.2016 / 15:27
fonte
38

Sim

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

É aqui que o idealismo se encontra com a realidade.

As pessoas semi-novas no SOLID e no cross-cutting muitas vezes enfrentam esse desafio mental. Tudo bem, não enlouqueça. Esforce-se para colocar tudo em termos de SOLID, mas existem alguns lugares, como o registro em log e o armazenamento em cache, onde o SOLID não faz sentido. Cross-cutting é irmão do SOLID, eles andam de mãos dadas.

    
por 13.05.2016 / 18:20
fonte
25

Para registro, acho que é. A criação de log é invasiva e geralmente não está relacionada à funcionalidade do serviço. É comum e bem entendido usar os padrões singleton do framework de logging. Se não, você está criando e injetando registradores em todos os lugares e você não quer isso.

Um dos problemas acima é que alguém dirá 'mas como posso testar o log?' . Meus pensamentos são que eu normalmente não testo o log, além de afirmar que eu posso realmente ler os arquivos de log e entendê-los. Quando eu vi logging testado, geralmente é porque alguém precisa afirmar que uma classe realmente fez algo e eles estão usando as mensagens de log para obter esse feedback. Eu preferiria registrar algum ouvinte / observador nessa classe e afirmar em meus testes que isso é chamado. Você pode então colocar seu log de eventos dentro desse observador.

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

    
por 13.05.2016 / 13:41
fonte
12

Meus 2 centavos ...

Sim e não.

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

Os princípios do SOLID em particular, além de exigir muitas nuances, são, em última análise, subservientes ao objetivo de "entregar software de trabalho e de manutenção". Portanto, aderir a qualquer princípio específico do SOLID é autodestrutivo e contraditório quando isso entra em conflito com os objetivos do SOLID. E aqui, muitas vezes observo que entrega supera manutenção .

Então, o que acontece com o D em SOLID ? Bem, isso contribui para uma maior manutenibilidade ao tornar seu módulo reutilizável relativamente agnóstico à sua contexto. E podemos definir o "módulo reutilizável" como "uma coleção de código 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 um "outro contexto distinto".

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

Primeiramente: 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 você não atualmente planeja usar um módulo em outro contexto, provavelmente está OK para que ele dependa disso indefesamente. A ressalva para a advertência: Seus planos provavelmente estão errados - mas isso é também OK. Quanto mais tempo você escrever módulo após módulo, mais e mais intuitivo e preciso será o sentido de "eu preciso disso novamente algum dia". Mas, provavelmente, você nunca poderá dizer retrospectivamente: "Eu modulei e desvinculei tudo o máximo possível, mas sem excesso."

Se você se sentir culpado por seus erros de julgamento, confesse-se e siga em frente ...

Em segundo lugar: Inverter o controle não é igual a injetando dependências .

Isso é especialmente verdadeiro quando você inicia injetando dependências ad nauseam . A Injeção de Dependência é uma tática útil para a estratégia global da IoC. Mas, eu diria que a ID é de menor eficácia do que algumas outras táticas - como usar interfaces e adaptadores - único pontos de exposição ao contexto a partir do módulo .

E vamos nos concentrar nisso por um segundo. Porque, mesmo que você injete um Logger ad nauseam , você precisa escrever código na interface Logger . Você não pode começar a usar um novo Logger de um fornecedor diferente que recebe parâmetros em uma ordem diferente. Essa habilidade vem da codificação, dentro do módulo, contra uma interface que existe dentro do módulo e que possui um único sub-módulo (Adapter) para gerenciar a dependência.

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

tl; dr - Pare de se preocupar com princípios sem considerar por que você está usando os princípios. E, de forma mais prática, basta criar um Adapter para cada módulo. Use o 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 registrador real no Adapter - mas não em cada pequena coisa que possa precisar dele.

    
por 13.05.2016 / 20:19
fonte
8

A idéia de que o registro deve ser sempre implementado como um singleton é uma daquelas mentiras que foram contadas com tanta frequência que ele ganhou força.

Desde que os sistemas operacionais modernos tenham sido reconhecidos, você pode querer 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 em novas. Se eles não estão realizando tal diligência, então eles não estão fazendo o seu trabalho.

    
por 13.05.2016 / 14:20
fonte
4

O registo é genuinamente um caso especial.

@Telastyn escreve:

Are you really never going to change your logging library, or target, or filtering, or formatting, or...?

Se você acredita que pode precisar alterar sua biblioteca de registro, deve estar usando uma fachada; ou seja, SLF4J se você estiver no mundo Java.

Quanto ao resto, uma biblioteca de registro decente cuida de mudar para onde o registro vai, quais eventos são filtrados, como os eventos de registro são formatados usando arquivos de configuração do registrador e (se necessário) classes de plugin customizadas. Existem várias alternativas disponíveis no mercado.

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

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

@Telastyn escreve:

And you'll then get the argument that the logger itself will handle the change. "Oh, if the target/filtering/formatting/strategy changes, then we'll just change the config!" That is garbage. Not only do you now have a God Object that handles all of these things, you're writing your code in XML (or similar) where you don't get static analysis, you don't get compile time errors, and you don't really get effective unit tests.

Eu tenho medo que seja um ripost muito teórico. Na prática, a maioria dos desenvolvedores e integradores de sistemas gostam do fato de que você pode configurar o log através de um arquivo de configuração. E eles gostam do fato de que eles não são esperados para a unidade de teste de registro de um módulo.

Claro, se você acumular as configurações de registro, poderá obter problemas, mas elas se manifestarão como o aplicativo falhando durante a inicialização ou com muito / muito pouco registro. 1) Esses problemas são facilmente corrigidos ao corrigir o erro no arquivo de configuração. 2) A alternativa é um ciclo completo de criação / análise / teste / implementação sempre que você fizer uma alteração nos níveis de log. Isso não é aceitável.

    
por 14.05.2016 / 03:59
fonte
3

Sim e Não !

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

No entanto,

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

Essa é uma das razões, entre muitas outras, para se pensar em modularidade em 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 dar considerações diferentes dentro dos limites modulares do que quando os cruzamos.

    
por 13.05.2016 / 19:10
fonte
3

Primeiro, começa com um cache de singleton strong, as próximas coisas que você vê são singletons strongs para a camada de banco de dados introduzindo 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 idéia ter um singleton para um cache, afinal, eles representam um conceito muito semelhante, armazenamento de dados, usando apenas mecanismos diferentes.

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

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

    
por 13.05.2016 / 21:37
fonte
3

Sim e Não, mas principalmente Não

Assumo que a maior parte da conversa é baseada em instâncias estáticas vs injetadas. Ninguém está propondo que o log de quebrar o SRP eu assumo? Estamos falando principalmente do "princípio da inversão de dependência". Na maioria das vezes, concordo com a resposta da Telastyn.

Quando está tudo bem em usar a estática? Porque claramente, às vezes, está tudo bem. O ponto de respostas afirmativas dos benefícios da abstração e as respostas do "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 em todas as situações.

Tome: Convert.ToInt32("1")

Eu prefiro isso para:

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 clareza valem esse comércio.

Olhando para o outro lado do espectro, se IConverter fosse um SqlConnection , ficaria bastante horrorizado ao ver isso como uma chamada estática. As razões pelas quais são fracassadas óbvias. Gostaria de salientar que um SQLConnection pode ser bastante "transversal" em um applciation, então eu não teria usado essas palavras exatas.

O registro está mais parecido com um SQLConnection ou Convert.ToInt32 ? Eu diria mais como 'SQLConnection'.

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

Eu também acho que é algo muito provável que mude. Um bom registro não apenas imprime strings, é uma visão do que seu aplicativo está fazendo (sou um grande fã do registro baseado em eventos). Eu vi log básico se transformar em UIs de relatórios bastante elaborados. Obviamente, é muito mais fácil seguir nessa direção se seu registro se parecer com _logger.Log(new ApplicationStartingEvent()) e menos como Logger.Log("Application has started") . Pode-se argumentar que isso está criando um inventário para um futuro que pode nunca acontecer, isso é um julgamento e eu acho que vale a pena.

Na verdade, em um projeto pessoal meu, criei uma interface de 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. Eu sinto que se a minha atitude em relação ao registro fosse simples e imutável, essa idéia não teria ocorrido para mim.

Então, eu concordaria com a Telastyn no caso de registro em log.

    
por 14.05.2016 / 19:41
fonte
3

As primeiras questões de corte cruzado não são os principais blocos de construção e não devem ser tratadas como dependências em um sistema. Um sistema deve funcionar se, e. O registrador não é inicializado ou o cache não está 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 seu ciclo de vida do objeto por quanto tempo você deseja que o objeto viva na memória.

Uma classe que precisa de uma dependência para inicializar não deve saber se a instância da classe fornecida é singleton ou transient. Mas tldr; Se você está escrevendo Logger.Instance.Log () em cada método ou classe, então o código problemático (código de cheiro / hard coupling) é muito confuso. 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.

    
por 15.05.2016 / 13:31
fonte
2

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

    
por 13.05.2016 / 16:24
fonte