Mover gradualmente a base de código para o contêiner de injeção de dependência

9

Eu tenho uma grande base de código com muitos singletons "antipadrão", classes de utilitário com métodos estáticos e classes criando suas próprias dependências usando a newpalavra-chave. Torna um código muito difícil de testar.

Eu quero migrar gradualmente o código para o contêiner de injeção de dependência (no meu caso Guice, porque é um GWTprojeto). Pelo meu entendimento da injeção de dependência, é tudo ou nada. Todas as classes são gerenciadas pelo Spring / Guice ou nenhuma. Como a base de código é grande, não consigo transformar o código durante a noite. Então, eu preciso de uma maneira de fazer isso gradualmente.

O problema é que, quando começo com uma classe que precisa ser injetada em outras classes, não posso usar um simples @Injectnessas classes, porque essas classes ainda não são gerenciadas por contêiner. Portanto, isso cria uma cadeia longa até as classes "principais" que não são injetadas em lugar algum.

A única maneira que vejo é disponibilizar Injectorglobalmente um contexto / aplicativo por meio de um singleton, para que outras classes possam obter um beans gerenciados a partir dele. Mas isso contradiz a ideia importante de não revelar composition roota aplicação.

Outra abordagem seria de baixo para cima: para começar com as classes "de alto nível", inclua-as no contêiner de injeção de dependência e vá lentamente para as classes "menores". Mas então tenho que esperar muito tempo, pois posso testar as classes menores que ainda dependem de globais / estáticas.

Qual seria o caminho para alcançar uma migração tão gradual?

PS A pergunta As abordagens graduais para injeção de dependência são semelhantes no título, mas não respondem à minha pergunta.

damluar
fonte
11
Deseja passar de classes com acoplamento rígido para o uso de um contêiner de injeção de dependência? Primeiro você precisa refatorar para dissociar as classes, usando interfaces antes de pensar em se casar com qualquer ferramenta ou estrutura de injeção de dependência.
Tulains Córdova
Justo. Então, eu tenho alguma classe de utilidade A que é usada em outras 30 classes (uma delas é a classe B), tornando-as não testáveis. Pensei em fazer da classe A uma dependência construtora da classe B. Tenho que mudar todas as chamadas para construtores e passar A para dentro. Mas de onde devo tirá-lo?
damluar
Se B é a menor unidade na qual você pode aplicar DI para começar, então não vejo mal em construir A onde quer que esteja construindo B por enquanto - desde que A não tenha dependências. Quando a bola estiver rolando, você pode voltar e refatorá-la.
Andy Hunt
@damluar Uma fábrica para começar?
Tulains Córdova
11
@damluar uma classe não se torna testável apenas porque depende de uma classe de utilitário. Significa apenas que sua unidade é um pouco maior do que você deseja. Se você puder testar sua classe de utilitário por si só, poderá usá-la em testes para todas as outras 30 classes sem se preocupar. Leia o blog de Martin Fowlers sobre teste de unidade.
Gbjbaanb

Respostas:

2

Desculpe, C#é o meu idioma de escolha, eu posso ler, Javamas provavelmente abafaria a sintaxe tentando escrevê-lo ... Os mesmos conceitos se aplicam entre C#e Javaembora, portanto, espero que isso mostre as etapas em como você pode mover gradualmente sua base de código para ser mais testável.

Dado:

public class MyUI
{
    public void SomeMethod()
    {
        Foo foo = new Foo();
        foo.DoStuff();
    }
}

public class Foo
{

    public void DoStuff()
    {
        Bar bar = new Bar();
        bar.DoSomethingElse();
    }

}

public class Bar
{
    public void DoSomethingElse();
}

pode ser facilmente refatorado para usar DI, sem o uso de um contêiner de IOC - e você pode até dividi-lo em várias etapas:

(potencial) Etapa um - receba dependências, mas nenhuma alteração no código de chamada (UI):

public class MyUI
{
    public void SomeMethod()
    {
        Foo foo = new Foo();
        foo.DoStuff();
    }
}

public class Foo
{

    private IBar _iBar;

    // Leaving this constructor for step one, 
    // so that calling code can stay as is without impact
    public Foo()
    {
        _iBar = new Bar();
    }

    // simply because we now have a constructor that take's in the implementation of the IBar dependency, 
    // Foo can be much more easily tested.
    public Foo(IBar iBar)
    {
        _iBar = iBar;
    }

    public void DoStuff()
    {
        _iBar.DoSomethingElse();
    }

}

public interface IBar
{
    void DoSomethingElse();
}

public class Bar
{
    public void DoSomethingElse();
}

Refatorar 2 (ou primeiro, se estiver implementando o contêiner IOC e alterando o código de chamada imediatamente):

public class MyUI
{
    public void SomeMethod()
    {
        Foo foo = null // use your IOC container to resolve the dependency
        foo.DoStuff();
    }
}

public class Foo
{

    private IBar _iBar;

    // note we have now dropped the "default constructor" - this is now a breaking change as far as the UI is concerned.
    // You can either do this all at once (do only step 2) or in a gradual manner (step 1, then step 2)

    // Only entry into class - requires passing in of class dependencies (IBar)
    public Foo(IBar iBar)
    {
        _iBar = iBar;
    }

    public void DoStuff()
    {
        _iBar.DoSomethingElse();
    }

}

Tecnicamente, a Etapa 2 poderia ser executada por si só - mas (potencialmente) seria muito mais trabalhosa - dependendo de quantas classes estão atualmente "atualizando" a funcionalidade que você está procurando DI.

Considere seguir a etapa 1 -> etapa 2 - para a qual você poderá criar testes de unidade Foo, independentemente de Bar. Considerando que antes da refatoração da etapa 1, não era facilmente realizado sem o uso da implementação real de ambas as classes. A execução das etapas 1 -> etapa 2 (em vez da etapa 2 imediatamente) permitiria alterações menores ao longo do tempo, e você já terá o início de um equipamento de teste para garantir melhor que seu refator funcione sem conseqüências.

Kritner
fonte
0

O conceito é o mesmo, esteja você usando Java, PHP ou mesmo C #. Esta questão é tratada bastante bem por Gemma Anible neste vídeo do YouTube:

https://www.youtube.com/watch?v=Jccq_Ti8Lck (PHP, desculpe!)

Você substitui o código não testável por "fachada" (por falta de um termo melhor) que chama código novo e testável. Gradualmente, você poderá substituir as chamadas antigas pelos serviços injetados. Eu fiz isso no passado e funciona bastante bem.

Kevin G.
fonte