contexto ambiental versus injeção de construtor

9

Eu tenho muitas classes principais que exigem ISessionContext do banco de dados, ILogManager para log e IService usado para se comunicar com outros serviços. Eu quero usar injeção de dependência para esta classe usada por todas as classes principais.

Eu tenho duas implementações possíveis. A classe principal que aceita IAmbientContext com todas as três classes ou injeta para todas as classes as três classes.

public interface ISessionContext 
{
    ...
}

public class MySessionContext: ISessionContext 
{
    ...
}

public interface ILogManager 
{

}

public class MyLogManager: ILogManager 
{
    ...
}

public interface IService 
{
    ...
}

public class MyService: IService
{
    ...
}

Primeira solução:

public class AmbientContext
{
    private ISessionContext sessionContext;
    private ILogManager logManager;
    private IService service;

    public AmbientContext(ISessionContext sessionContext, ILogManager logManager, IService service)
    {
        this.sessionContext = sessionContext;
        this.logManager = logManager;
        this.service = service;
    }
}


public class MyCoreClass(AmbientContext ambientContext)
{
    ...
}

segunda solução (sem ambiente)

public MyCoreClass(ISessionContext sessionContext, ILogManager logManager, IService service)
{
    ...
}

Qual é a melhor solução neste caso?

user3401335
fonte
O que é " IServiceusado para se comunicar com outros serviços?" Se IServicerepresenta uma dependência vaga de outros serviços, parece um localizador de serviço e não deveria existir. Sua classe deve depender de interfaces que descrevam explicitamente o que o consumidor fará com eles. Nenhuma classe precisa de um serviço para fornecer acesso a um serviço. Uma classe precisa de uma dependência que faça algo específico que a classe precisa.
Scott Hannen

Respostas:

4

"Melhor" é muito subjetivo aqui. Como é comum em tais decisões, é uma troca entre duas maneiras igualmente válidas de alcançar algo.

Se você criar AmbientContexte injetar isso em muitas classes, você estará potencialmente fornecendo mais informações a cada uma delas do que elas precisam (por exemplo, a classe Foosó pode usar ISessionContext, mas está sendo informada ILogManagere ISessiontambém).

Se você passar cada um deles através de um parâmetro, então diz a cada classe apenas as coisas que ele precisa saber. Porém, o número de parâmetros pode crescer rapidamente e você pode achar que tem muitos construtores e métodos com muitos parâmetros altamente repetidos, que podem ser simplificados por meio de uma classe de contexto.

Portanto, é um caso de equilibrar os dois e escolher o apropriado para as suas circunstâncias. Se você tiver apenas uma classe e três parâmetros, eu pessoalmente não me importaria AmbientContext. Para mim, o ponto de inflexão provavelmente seria quatro parâmetros. Mas isso é pura opinião. Seu ponto de inflexão provavelmente será diferente do meu, então escolha o que lhe parecer melhor.

David Arno
fonte
4

A terminologia da pergunta realmente não corresponde ao código de exemplo. O Ambient Contexté um padrão usado para capturar uma dependência de qualquer classe em qualquer módulo o mais fácil possível, sem poluir todas as classes para aceitar a interface da dependência, mas ainda mantendo a idéia de inversão de controle. Tais dependências geralmente são dedicadas ao registro, segurança, gerenciamento de sessões, transações, armazenamento em cache, auditoria, para qualquer preocupação transversal nesse aplicativo. É de alguma forma irritante para adicionar um ILogging, ISecurity, ITimeProviderpara construtores e na maioria das vezes nem todas as classes precisam tudo ao mesmo tempo, então eu entendo a sua necessidade.

E se o tempo de vida da ISessioninstância for diferente ILoggerdaquele? Talvez a instância ISession deva ser criada em cada solicitação e no ILogger uma vez. Portanto, ter todas essas dependências governadas por um objeto que não é o próprio contêiner não parece a escolha certa, devido a todos esses problemas com gerenciamento e localização de vida útil e outros descritos neste encadeamento.

A IAmbientContextquestão não resolve o problema de não poluir todos os construtores. Você ainda precisa usá-lo na assinatura do construtor, com certeza, apenas uma vez dessa vez.

Portanto, a maneira mais fácil NÃO é usar a injeção de construtor ou qualquer outro mecanismo de injeção para lidar com dependências transversais, mas usando uma chamada estática . Na verdade, vemos esse padrão com bastante frequência, implementado pela própria estrutura. Verifique Thread.CurrentPrincipal, que é uma propriedade estática que retorna uma implementação da IPrincipalinterface. Também é configurável para que você possa alterar a implementação, se assim o desejar, para que não esteja acoplado a ela.

MyCore parece agora algo como

public class MyCoreClass
{
    public void BusinessFeature(string data)
    {
        LoggerContext.Current.Log(data);

        _repository.SaveProcessedData();

        SessionContext.Current.SetData(data);
        ...etc
    }
}

Esse padrão e possíveis implementações foram descritos em detalhes por Mark Seemann neste artigo . Pode haver implementações que dependem do próprio contêiner de IoC que você usa.

Você deseja evitar AmbientContext.Current.Logger, AmbientContext.Current.Sessionpelas mesmas razões descritas acima.

Mas você tem outras opções para resolver esse problema: use decoradores, interceptação dinâmica se o seu contêiner tiver esse recurso ou AOP. O contexto ambiental deve ser o último recurso, pois seus clientes ocultam suas dependências por meio dele. Eu ainda usaria o Contexto ambiente se a interface realmente imitar meu impulso de usar uma dependência estática como DateTime.Nowou ConfigurationManager.AppSettingse essa necessidade aumentar com bastante frequência. Mas, no final, a injeção do construtor pode não ser uma má idéia para obter essas dependências onipresentes.

Adrian Iftode
fonte
3

Eu evitaria o AmbientContext.

Primeiro, se a classe depende AmbientContext, você não sabe o que faz. Você deve observar o uso dessa dependência para descobrir qual das dependências aninhadas ela usa. Você também não pode analisar o número de dependências e dizer se sua classe está fazendo muito porque uma dessas dependências pode realmente representar várias dependências aninhadas.

Segundo, se você o estiver usando para evitar várias dependências de construtores, essa abordagem incentivará outros desenvolvedores (incluindo você) a adicionar novos membros a essa classe de contexto ambiental. Então o primeiro problema é composto.

Terceiro, zombar da dependência AmbientContexté mais difícil, porque é necessário descobrir em cada caso se deve zombar de todos os membros ou apenas dos que você precisa e, em seguida, criar um zombaria que retorne essas zombarias (ou teste em dobro). sua unidade testa mais difícil escrever, ler e manter.

Quarto, falta coesão e viola o princípio da responsabilidade única. É por isso que ele tem um nome como "AmbientContext", porque faz muitas coisas não relacionadas e não há como nomeá-lo de acordo com o que faz.

E provavelmente viola o Princípio de Segregação de Interface, introduzindo membros da interface em classes que não precisam deles.

Scott Hannen
fonte
2

O segundo (sem o wrapper da interface)

A menos que haja alguma interação entre os vários serviços que precisam ser encapsulados em uma classe intermediária, isso apenas complica seu código e limita a flexibilidade quando você introduz a 'interface de interfaces'

Ewan
fonte