Como usar a injeção de dependência e evitar o acoplamento temporal?

11

Suponha que eu tenha o Serviceque recebe dependências via construtor, mas também precise ser inicializado com dados personalizados (contexto) antes de poder ser usado:

public interface IService
{
    void Initialize(Context context);
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3)
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
    }

    public void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

public class Context
{
    public int Value1;
    public string Value2;
    public string Value3;
}

Agora - os dados de contexto não são conhecidos de antemão, portanto, não posso registrá-los como uma dependência e usar o DI para injetá-los no serviço

É assim que o exemplo de cliente se parece:

public class Client
{
    private readonly IService service;

    public Client(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public void OnStartup()
    {
        service.Initialize(new Context
        {
            Value1 = 123,
            Value2 = "my data",
            Value3 = "abcd"
        });
    }

    public void Execute()
    {
        service.DoSomething();
        service.DoOtherThing();
    }
}

Como você pode ver - existem acoplamentos temporais e odores de código de método de inicialização envolvidos, porque primeiro eu preciso ligar service.Initializepara poder ligar service.DoSomethinge service.DoOtherThingdepois.

Quais são as outras abordagens nas quais posso eliminar esses problemas?

Esclarecimentos adicionais sobre o comportamento:

Cada instância do cliente precisa ter sua própria instância do serviço inicializada com dados de contexto específicos do cliente. Portanto, esses dados de contexto não são estáticos ou são conhecidos antecipadamente, portanto, não podem ser injetados por DI no construtor.

Dusan
fonte

Respostas:

18

Existem várias maneiras de lidar com o problema de inicialização:

  • Conforme respondido em https://softwareengineering.stackexchange.com/a/334994/301401 , os métodos init () são um cheiro de código. A inicialização de um objeto é de responsabilidade do construtor - é por isso que temos construtores, afinal.
  • Adicionar O serviço fornecido deve ser inicializado no comentário de doc do Clientconstrutor e deixar o construtor lançar se o serviço não for inicializado. Isso move a responsabilidade para quem lhe dá o IServiceobjeto.

No entanto, no seu exemplo, Clienté o único que conhece os valores que são transmitidos Initialize(). Se você quiser mantê-lo assim, sugiro o seguinte:

  • Adicione um IServiceFactorye passe-o para o Clientconstrutor. Em seguida, você pode ligar, o serviceFactory.createService(new Context(...))que fornece uma inicialização IServiceque pode ser usada pelo seu cliente.

As fábricas podem ser muito simples e também permitem evitar métodos init () e usar construtores:

public interface IServiceFactory
{
    IService createService(Context context);
}

public class ServiceFactory : IServiceFactory
{
    public Service createService(Context context)
    {
        return new Service(context);
    }
}

No cliente, OnStartup()também é um método de inicialização (ele usa apenas um nome diferente). Portanto, se possível (se você conhece os Contextdados), a fábrica deve ser chamada diretamente no Clientconstrutor. Se isso não for possível, você precisa armazenar IServiceFactorye chamá-lo OnStartup().

Quando Servicetem dependências não fornecidas por Clientelas, elas seriam fornecidas pela DI através de ServiceFactory:

public interface IServiceFactory
{
    IService createService(Context context);
}    

public class ServiceFactory : IServiceFactory
{        
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public ServiceFactory(object dependency1, object dependency2, object dependency3)
    {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
        this.dependency3 = dependency3;
    }

    public Service createService(Context context)
    {
        return new Service(context, dependency1, dependency2, dependency3);
    }
}
pschill
fonte
1
Obrigado, como eu pensava, no último ponto ... E no ServiceFactory, você usaria o construtor DI na própria fábrica para as dependências necessárias para o construtor de serviços ou o localizador de serviços seria mais adequado?
Dusan
1
@Dusan não usa o Service Locator. Se Servicetiver dependências diferentes de Context, que não seriam fornecidas pelo Client, elas podem ser fornecidas via DI ServiceFactorypara serem transmitidas ao Servicequando createServicefor chamado.
Mr.Mindor 2/19/19
@Dusan Se você precisar fornecer dependências diferentes para diferentes Serviços (por exemplo: este precisa da dependência1_1, mas o próximo precisa da dependência1_2), mas se esse padrão funcionar para você, você poderá usar um padrão semelhante, geralmente chamado de padrão Builder. Um Construtor permite que você configure um objeto aos poucos, se necessário. Em seguida, você pode fazer isso ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);e ficar com o seu serviço parcialmente configurado, e depois fazerService s = partial.context(context).build()
Aaron
1

O Initializemétodo deve ser removido da IServiceinterface, pois esse é um detalhe da implementação. Em vez disso, defina outra classe que pegue a instância concreta do Service e chame o método initialize nela. Então essa nova classe implementa a interface IService:

public class ContextDependentService : IService
{
    public ContextDependentService(Context context, Service service)
    {
        this.service = service;

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Isso mantém o código do cliente ignorante do procedimento de inicialização, exceto onde a ContextDependentServiceclasse é inicializada. Você pelo menos limita as partes do seu aplicativo que precisam saber sobre esse procedimento de inicialização complicado.

Greg Burghardt
fonte
1

Parece-me que você tem duas opções aqui

  1. Mova o código de Inicialização para o Contexto e injete um Contexto Inicializado

por exemplo.

public InitialisedContext Initialise()
  1. Faça a primeira chamada para Executar Inicialização da Chamada, se ainda não estiver concluída.

por exemplo.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Basta lançar exceções se o contexto não for inicializado quando você chamar Execute. Como SqlConnection.

Injetar uma fábrica é bom se você quiser evitar passar o contexto como parâmetro. Digamos que apenas essa implementação específica precise de um contexto e você não queira adicioná-lo à Interface

Mas você basicamente tem o mesmo problema, e se a fábrica ainda não tiver um contexto inicializado.

Ewan
fonte
0

Você não deve depender sua interface para qualquer contexto de banco de dados e método de inicialização. Você pode fazer isso no construtor de classe concreto.

public interface IService
{
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;
    private readonly object context;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3,
        object context )
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));

        // context is concrete class details not interfaces.
        this.context = context;

        // call init here constructor.
        this.Initialize(context);
    }

    protected void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

E, uma resposta para sua pergunta principal seria Injeção de propriedades .

public class Service
    {
        public Service(Context context)
        {
            this.context = context;
        }

        private Dependency1 _dependency1;
        public Dependency1 Dependency1
        {
            get
            {
                if (_dependency1 == null)
                    _dependency1 = Container.Resolve<Dependency1>();

                return _dependency1;
            }
        }

        //...
    }

Dessa forma, você pode chamar todas as dependências por Injeção de propriedades . Mas poderia ser um número enorme. Nesse caso, você pode usar a Injeção de construtor para eles, mas pode definir seu contexto por propriedade, verificando se é nulo.

Engineert
fonte
OK, ótimo, mas ... cada instância do cliente precisa ter sua própria instância do serviço inicializada com dados de contexto diferentes. Esses dados de contexto não são estáticos ou são conhecidos antecipadamente, portanto, não podem ser injetados por DI no construtor. Então, como faço para obter / criar instância do serviço junto com outras dependências em meus clientes?
Dusan
hmm esse construtor estático não será executado antes de você definir o contexto? e inicializar no construtor riscos exceções
Ewan
Estou inclinado a injetar fábrica que pode criar e inicializar o serviço com os dados de contexto fornecidos (em vez de injetar o próprio serviço), mas não tenho certeza se existem soluções melhores.
Dusan
@ Ewan Você está certo. Vou tentar encontrar uma solução para isso. Mas antes disso, vou removê-lo por enquanto.
Engineert
0

Misko Hevery tem uma postagem de blog muito útil sobre o caso que você enfrentou. Vocês dois precisam ser renováveis e injetáveis para a sua Serviceturma, e esta postagem no blog pode ajudá-lo.

Negrito P.
fonte