Existe um padrão para inicializar objetos criados por meio de um contêiner de DI


147

Estou tentando fazer com que o Unity gerencie a criação dos meus objetos e quero ter alguns parâmetros de inicialização que não são conhecidos até o tempo de execução:

No momento, a única maneira de pensar em como fazer isso é ter um método Init na interface.

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Então, para usá-lo (no Unity), eu faria o seguinte:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

Nesse cenário, o runTimeParamparâmetro é determinado no tempo de execução com base na entrada do usuário. O caso trivial aqui simplesmente retorna o valor de, runTimeParammas, na realidade, o parâmetro será algo como o nome do arquivo e o método de inicialização fará algo com o arquivo.

Isso cria vários problemas, a saber, que o Initializemétodo está disponível na interface e pode ser chamado várias vezes. Definir um sinalizador na implementação e lançar exceção em chamadas repetidas para Initializeparece muito desajeitado.

No momento em que resolvo minha interface, não quero saber nada sobre a implementação do IMyIntf. O que eu quero, no entanto, é o conhecimento de que essa interface precisa de determinados parâmetros de inicialização únicos. Existe uma maneira de anotar (atributos?) A interface com essas informações e transmiti-las à estrutura quando o objeto é criado?

Edit: Descreveu a interface um pouco mais.


9
Você está perdendo o objetivo de usar um contêiner de DI. As dependências devem ser resolvidas para você.
Pierreten

De onde você está obtendo os parâmetros necessários? (config arquivo, db, ??)
Jaime

runTimeParamé uma dependência que é determinada no tempo de execução com base em uma entrada do usuário. A alternativa para isso deve ser dividida em duas interfaces - uma para inicialização e outra para armazenar valores?
Igor Zevaka

dependência na IoC, geralmente se refere à dependência de outras classes ou objetos do tipo ref que podem ser determinados na fase de inicialização da IoC. Se sua classe precisa apenas de alguns valores para funcionar, é aí que o método Initialize () da sua classe se torna útil.
The Light

Quero dizer, imagine que existem 100 classes no seu aplicativo nas quais essa abordagem pode ser aplicada; você precisará criar 100 classes de fábrica extras + 100 interfaces para suas classes e poderá se safar se estivesse usando o método Initialize ().
The Light

Respostas:


276

Qualquer local em que você precise de um valor em tempo de execução para construir uma dependência específica, o Abstract Factory é a solução.

A inicialização de métodos nas interfaces cheira a uma abstração com vazamento .

No seu caso, eu diria que você deve modelar a IMyIntfinterface sobre como precisa usá-la - e não sobre como pretende criar implementações. Esse é um detalhe de implementação.

Assim, a interface deve ser simplesmente:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Agora defina a Fábrica abstrata:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Agora você pode criar uma implementação concreta IMyIntfFactoryque cria instâncias concretas IMyIntfcomo esta:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Observe como isso nos permite proteger os invariantes da classe usando a readonlypalavra - chave. Nenhum método de inicialização fedorento é necessário.

Uma IMyIntfFactoryimplementação pode ser tão simples quanto isto:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

Em todos os seus clientes em que você precisa de uma IMyIntfinstância, basta assumir uma dependência IMyIntfFactorysolicitando-a através da Injeção de Construtor .

Qualquer contêiner de DI que se preze poderá ligar automaticamente uma IMyIntfFactoryinstância para você, se você a registrar corretamente.


13
O problema é que um método (como Initialize) faz parte da sua API, enquanto o construtor não é. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann

12
Além disso, um método Initialize indica o acoplamento temporal: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann

2
@Darlene Você pode usar um Decorator inicializado preguiçosamente, conforme descrito na seção 8.3.6 do meu livro . Também forneço um exemplo de algo semelhante em minha apresentação Big Object Graphs Up Front .
Mark Seemann

2
@ Mark Se a criação da MyIntfimplementação da fábrica exigir mais do que runTimeParam(leia-se: outros serviços que alguém gostaria que fossem resolvidos por uma IoC), você ainda terá que resolver essas dependências na sua fábrica. Gosto da resposta do @PhilSandler de passar essas dependências para o construtor da fábrica para resolver isso - essa é a sua opinião também?
Jeff

2
Também são ótimas coisas, mas sua resposta a essa outra pergunta realmente chegou ao meu ponto.
8113 Jeff

15

Geralmente, quando você encontra essa situação, precisa revisitar seu design e determinar se está misturando seus objetos de estado / dados com seus serviços puros. Na maioria dos casos (não todos), você deseja manter esses dois tipos de objetos separados.

Se você precisar de um parâmetro específico do contexto passado no construtor, uma opção é criar uma fábrica que resolva suas dependências de serviço por meio do construtor e use o parâmetro de tempo de execução como um parâmetro do método Create () (ou Generate ( ), Build () ou qualquer que seja o nome dos métodos de fábrica).

Ter setters ou um método Initialize () geralmente é considerado um design ruim, pois você precisa "lembrar" de chamá-los e garantir que eles não abram muito o estado da sua implementação (ou seja, o que impede alguém de chamada inicializando ou o levantador?).


5

Também me deparei com essa situação algumas vezes em ambientes em que estou criando dinamicamente objetos ViewModel baseados em objetos Model (descritos muito bem por essa outra postagem do Stackoverflow ).

Gostei de como a extensão Ninject, que permite criar fábricas dinamicamente com base em interfaces:

Bind<IMyFactory>().ToFactory();

Não encontrei nenhuma funcionalidade semelhante diretamente no Unity ; então escrevi minha própria extensão no IUnityContainer, que permite registrar fábricas que criarão novos objetos com base em dados de objetos existentes, mapeando essencialmente de uma hierarquia de tipos para uma hierarquia de tipos diferente: UnityMappingFactory @ GitHub

Com o objetivo de simplicidade e legibilidade, terminei com uma extensão que permite especificar diretamente os mapeamentos sem declarar classes ou interfaces individuais de fábrica (economia de tempo real). Você acabou de adicionar os mapeamentos exatamente onde registra as classes durante o processo normal de inicialização ...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Em seguida, basta declarar a interface da fábrica de mapeamento no construtor para CI e usar seu método Create () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

Como um bônus adicional, quaisquer dependências adicionais no construtor das classes mapeadas também serão resolvidas durante a criação do objeto.

Obviamente, isso não resolverá todos os problemas, mas me serviu muito bem até agora, então pensei que deveria compartilhá-lo. Há mais documentação no site do projeto no GitHub.


1

Não consigo responder com terminologia específica do Unity, mas parece que você está apenas aprendendo sobre injeção de dependência. Nesse caso, sugiro que você leia o guia do usuário breve, claro e informativo do Ninject .

Isso o guiará pelas várias opções disponíveis ao usar o DI e como explicar os problemas específicos que você enfrentará ao longo do caminho. No seu caso, você provavelmente desejaria usar o contêiner de DI para instanciar seus objetos e fazer com que esse objeto obtenha uma referência a cada uma de suas dependências por meio do construtor.

A explicação passo a passo também detalha como anotar métodos, propriedades e até parâmetros usando atributos para distingui-los no tempo de execução.

Mesmo se você não usar o Ninject, o passo a passo fornecerá os conceitos e a terminologia da funcionalidade adequada ao seu objetivo, e você poderá mapear esse conhecimento para o Unity ou outras estruturas de DI (ou convencê-lo a experimentar o Ninject) .


Obrigado por isso. Na verdade, estou avaliando estruturas de DI e o NInject seria o meu próximo.
Igor Zevaka


1

Eu acho que resolvi e parece bastante saudável, então deve estar meio certo :))

Dividi-me IMyIntfnas interfaces "getter" e "setter". Assim:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Então a implementação:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()ainda pode ser chamado várias vezes, mas usando bits do paradigma Service Locator , podemos agrupá-lo bastante bem, de modo que IMyIntfSetteré quase uma interface interna distinta IMyIntf.


13
Essa não é uma solução particularmente boa, pois depende de um método Initialize, que é uma abstração com vazamento. Aliás, isso não se parece com o Localizador de Serviço, mas mais com a Injeção de Interface. De qualquer forma, veja minha resposta para uma solução melhor.
22220 Mark Seemann
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.