COI / DI - Por que tenho que fazer referência a todas as camadas / montagens no ponto de entrada do aplicativo?

123

(Relacionado a esta pergunta, EF4: Por que a criação de proxy precisa ser ativada quando o carregamento lento está ativado? ).

Eu sou novo no DI, então tenha paciência comigo. Entendo que o contêiner é responsável por instanciar todos os meus tipos registrados, mas para isso, é necessário fazer referência a todas as DLLs da minha solução e suas referências.

Se eu não estivesse usando um contêiner de DI, não precisaria fazer referência à biblioteca EntityFramework no meu aplicativo MVC3, apenas minha camada de negócios, que faria referência à minha camada DAL / Repo.

Sei que no final do dia todas as DLLs estão incluídas na pasta bin, mas meu problema é ter que fazer referência explícita a ela através de "add reference" no VS para poder publicar um WAP com todos os arquivos necessários.

diegohb
fonte
1
Este trecho do livro Injection Dependency in .NET, second edition é uma versão mais elaborada das respostas de Mark e de mim. Ele descreve em detalhes o conceito da Raiz de Composição e por que deixar o caminho de inicialização do aplicativo depender de todos os outros módulos é realmente uma coisa boa.
1111 Steven
Li esse trecho do link e, no capítulo 1, comprarei o livro, pois realmente gostei das analogias e explicações simples da complexa questão da DI. Eu acho que você deveria sugerir uma nova resposta, responder com clareza "você não precisa fazer referência a todas as camadas / montagens na camada lógica de entrada, a menos que também seja a raiz da sua composição", vincule o trecho e publique a imagem Figura 3, no excerto.
Diegohb 12/0518

Respostas:

194

Se eu não estivesse usando um contêiner de DI, não precisaria fazer referência à biblioteca EntityFramework no meu aplicativo MVC3, apenas minha camada de negócios que referenciaria minha camada DAL / Repo.

Sim, é exatamente a situação que a DI trabalha tanto para evitar :)

Com código fortemente acoplado, cada biblioteca pode ter apenas algumas referências, mas estas novamente têm outras referências, criando um gráfico profundo de dependências, como este:

Gráfico profundo

Uma vez que o gráfico de dependência é profunda, que significa que a maioria das bibliotecas arrastar ao longo de uma série de outras dependências - por exemplo, no diagrama, Biblioteca C arrasta Biblioteca H, Biblioteca E, Biblioteca J, Biblioteca H, Biblioteca K e Biblioteca N . Isso dificulta a reutilização de cada biblioteca independentemente do restante - por exemplo, em testes de unidade .

No entanto, em um aplicativo fracamente acoplado, movendo todas as referências para a Raiz da Composição , o gráfico de dependência é achatado severamente :

Gráfico raso

Conforme ilustrado pela cor verde, agora é possível reutilizar a Biblioteca C sem arrastar as dependências indesejadas.

No entanto, tudo isso dito, com muitos DI Containers, você não precisa adicionar referências concretas a todas as bibliotecas necessárias. Em vez disso, você pode usar a ligação tardia na forma de varredura de montagem baseada em convenção (preferencial) ou configuração XML.

Ao fazer isso, no entanto, lembre-se de copiar os assemblies para a pasta bin do aplicativo, porque isso não acontece mais automaticamente. Pessoalmente, raramente acho que vale a pena esse esforço extra.

Uma versão mais elaborada dessa resposta pode ser encontrada neste trecho do meu livro Injection Dependency, Principles, Practices, Patterns .

Mark Seemann
fonte
3
Muito obrigado, isso agora faz todo o sentido .. eu precisava saber se isso era por design. Quanto a impor o uso correto de dependências, eu havia implementado um projeto separado com meu bootstrapper de DI, como Steven mencionado abaixo, onde faço referência ao restante das bibliotecas. Esse projeto é referenciado pelo aplicativo de ponto de entrada e, no final da compilação completa, faz com que todas as dlls necessárias estejam na pasta bin. obrigado!
Diegohb #
2
@ Mark Seemann Esta pergunta / resposta é específica da Microsoft? Gostaria de saber se essa idéia de mover todas as dependências para o "ponto de entrada do aplicativo" faz sentido para um projeto Java EE / Spring usando Maven ... obrigado!
Grégoire C
5
Esta resposta se aplica além do .NET. Você pode consultar o capítulo Princípios de Design de Pacotes, de Robert C. Martin, por exemplo , Desenvolvimento Ágil de Software, Princípios, Padrões e Práticas
Mark Seemann
7
@AndyDangerGagne A raiz da composição é um padrão DI - o oposto do Service Locator . Do ponto de vista da raiz da composição, nenhum dos tipos é polimórfico; a raiz da composição vê todos os tipos como tipos concretos e, portanto, o princípio de substituição de Liskov não se aplica a ele.
Mark27
4
Como regra geral, as interfaces devem ser definidas pelos clientes que as utilizam ( APP, cap. 11 ); portanto, se a Biblioteca J precisar de uma interface, ela deverá ser definida na Biblioteca J. Esse é um corolário do Princípio da Inversão da Dependência.
68668 Mark-Markmann
65

Se eu não estivesse usando um contêiner de DI, não precisaria fazer referência à biblioteca EntityFramework no meu aplicativo MVC3

Mesmo ao usar um contêiner de DI, você não precisa permitir que seu projeto MVC3 faça referência à EF, mas você (implicitamente) escolhe fazer isso implementando a Raiz de Composição (o caminho de inicialização onde você compõe seus gráficos de objeto) dentro de seu projeto MVC3. Se você for muito rigoroso quanto à proteção de seus limites de arquitetura usando montagens, poderá mover sua lógica de apresentação para um projeto diferente.

Quando você move toda a lógica relacionada ao MVC (controladores, etc.) do projeto de inicialização para uma biblioteca de classes, ele permite que esse conjunto da camada de apresentação fique desconectado do restante do aplicativo. Seu próprio projeto de aplicativo da Web se tornará um shell muito fino com a lógica de inicialização necessária. O projeto de aplicativo da Web será a Raiz da Composição que referencia todos os outros assemblies.

Extrair a lógica de apresentação para uma biblioteca de classes pode complicar as coisas ao trabalhar com o MVC. Será mais difícil conectar tudo, pois os controladores não estão no projeto de inicialização (enquanto visualizações, imagens, arquivos css provavelmente devem permanecer no projeto de inicialização). Provavelmente isso é possível, mas levará mais tempo para configurar.

Por causa das desvantagens, geralmente aconselho manter apenas a raiz da composição no projeto da web. Muitos desenvolvedores não querem que seu assembly MVC dependa do assembly DAL, mas isso não é realmente um problema. Não esqueça que montagens são um artefato de implantação ; você divide o código em vários assemblies para permitir que o código seja implantado separadamente. Uma camada arquitetônica, por outro lado, é um artefato lógico . É muito possível (e comum) ter várias camadas na mesma montagem.

Nesse caso, teremos a raiz da composição (camada) e a camada de apresentação no mesmo projeto de aplicativo da web (portanto, no mesmo assembly). E mesmo que esse assembly faça referência ao assembly que contém o DAL, a camada de apresentação ainda não faz referência à camada de acesso a dados . Esta é uma grande distinção.

Obviamente, quando fazemos isso, perdemos a capacidade do compilador de verificar essa regra de arquitetura em tempo de compilação, mas isso não deve ser um problema. A maioria das regras de arquitetura não pode ser verificada pelo compilador e sempre há algo como bom senso. E se não houver senso comum em sua equipe, você sempre poderá usar revisões de código (que toda equipe deve IMO sempre fazer). Você também pode usar uma ferramenta como o NDepend (que é comercial), que ajuda a verificar suas regras de arquitetura. Quando você integra o NDepend ao seu processo de construção, ele pode avisá-lo quando alguém fizer o check-in de um código que viole essa regra de arquitetura.

Você pode ler uma discussão mais elaborada sobre como a Raiz da Composição funciona no capítulo 4 do meu livro Injeção de Dependência, Princípios, Práticas, Padrões .

Steven
fonte
Um projeto separado para bootstrap foi a minha solução, já que não temos ndepend e nunca o usei antes. Vou analisá-lo, já que parece uma maneira melhor de realizar o que estou tentando fazer quando houver apenas 1 aplicativo final.
Diegohb
1
O último parágrafo é excelente e está começando a me ajudar a mudar de idéia sobre o quão rigoroso sou em manter as camadas em montagens separadas. Ter duas ou mais camadas lógicas em um assembly é realmente bom se você empregar outros processos em torno da escrita do código (como revisões de código) para garantir que não haja referência às classes DAL no código da UI e vice-versa.
BenM 16/09
6

Se eu não estivesse usando um contêiner de DI, não precisaria fazer referência à biblioteca EntityFramework no meu aplicativo MVC3, apenas minha camada de negócios que referenciaria minha camada DAL / Repo.

Você pode criar um projeto separado chamado "DependencyResolver". Neste projeto, você deve fazer referência a todas as suas bibliotecas.

Agora, a camada de interface do usuário não precisa de NHibernate / EF ou qualquer outra biblioteca não relevante para a interface do usuário, exceto Castle Windsor para ser referenciada.

Se você deseja ocultar Castle Windsor e DependencyResolver da camada da interface do usuário, você pode escrever um HttpModule que chama o material de registro IoC.

Eu tenho apenas um exemplo para StructureMap:

public class DependencyRegistrarModule : IHttpModule
{
    private static bool _dependenciesRegistered;
    private static readonly object Lock = new object();

    public void Init(HttpApplication context)
    {
        context.BeginRequest += (sender, args) => EnsureDependenciesRegistered();
    }

    public void Dispose() { }

    private static void EnsureDependenciesRegistered()
    {
        if (!_dependenciesRegistered)
        {
            lock (Lock)
            {
                if (!_dependenciesRegistered)
                {
                    ObjectFactory.ResetDefaults();

                    // Register all you dependencies here
                    ObjectFactory.Initialize(x => x.AddRegistry(new DependencyRegistry()));

                    new InitiailizeDefaultFactories().Configure();
                    _dependenciesRegistered = true;
                }
            }
        }
    }
}

public class InitiailizeDefaultFactories
{
    public void Configure()
    {
        StructureMapControllerFactory.GetController = type => ObjectFactory.GetInstance(type);
          ...
    }
 }

O DefaultControllerFactory não usa o contêiner de IoC diretamente, mas delega para os métodos de contêiner de IoC.

public class StructureMapControllerFactory : DefaultControllerFactory
{
    public static Func<Type, object> GetController = type =>
    {
        throw new  InvalidOperationException("The dependency callback for the StructureMapControllerFactory is not configured!");
    };

    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null)
        {
            return base.GetControllerInstance(requestContext, controllerType);
        }
        return GetController(controllerType) as Controller;
    }
}

O GetControllerdelegado é definido em um registro do StructureMap (em Windsor, deve ser um instalador).

Rookian
fonte
1
Eu gosto disso ainda melhor do que acabei fazendo, os módulos são ótimos. Então, onde eu faria a chamada para Container.Dispose ()? Evento ApplicationEnd ou EndRequest dentro do módulo ...?
Diegohb
1
@Steven Porque o Global.asax está na sua camada de interface do usuário do MVC. O HttpModule estaria no projeto DependencyResolver.
Rookian
1
O pequeno benefício é que ninguém pode usar o contêiner de IoC na interface do usuário. Ou seja, ninguém é capaz de usar o IoC Container como um localizador de serviço na interface do usuário.
Rookian
1
Além disso, ele proíbe que os desenvolvedores usem acidentalmente o código DAL na camada da interface do usuário, pois não há nenhuma referência rígida ao assembly na interface do usuário.
Diegohb #
1
Eu descobri como fazer a mesma coisa usando a API de registro genérico do Bootstrapper. Meu projeto de interface do usuário faz referência ao Bootstrapper, o projeto de resolução de dependência em que eu ligo meus registros e projetos no meu Core (para as interfaces), mas nada mais, nem mesmo meu DI Framework (SimpleInjector). Estou usando o nuget OutputTo para copiar dlls para a pasta bin.
Diegohb
0
  • Existe uma dependência: se um objeto instancia outro objeto.
  • Não há dependência: se um objeto espera uma abstração (injeção de construtor, injeção de método ...)
  • As referências de montagem (referenciando dll, serviços da web ...) são independentes do conceito de dependência, porque para resolver uma abstração e poder compilar o código, a camada deve fazer referência a ela.
riadh gomri
fonte