Déplacer progressivement la base de code vers le conteneur d'injection de dépendance

9

J'ai une grande base de code avec beaucoup de singletons "anti-pattern", des classes utilitaires avec des méthodes statiques et des classes créant leurs propres dépendances à l'aide de newmots-clés. Cela rend un code très difficile à tester.

Je veux migrer progressivement le code vers le conteneur d'injection de dépendance (dans mon cas, c'est Guiceparce que c'est un GWTprojet). D'après ma compréhension de l'injection de dépendance, c'est tout ou rien. Soit toutes les classes sont gérées par Spring / Guice, soit aucune. Étant donné que la base de code est grande, je ne peux pas transformer le code pendant la nuit. J'ai donc besoin d'un moyen de le faire progressivement.

Le problème est que lorsque je commence avec une classe qui doit être injectée dans d'autres classes, je ne peux pas utiliser de simple @Injectdans ces classes, car ces classes ne sont pas encore gérées par conteneur. Cela crée donc une longue chaîne jusqu'aux classes "supérieures" qui ne sont injectées nulle part.

La seule façon que je vois est de rendre un Injectorcontexte / application globalement disponible via un singleton pour le moment, afin que d'autres classes puissent en obtenir un beans gérés. Mais cela contredit l'idée importante de ne pas révéler le composition rootà l'application.

Une autre approche serait ascendante: pour commencer avec des classes "de haut niveau", incluez-les dans le conteneur d'injection de dépendance et descendez lentement vers des classes "plus petites". Mais alors je dois attendre longtemps, car je peux tester ces classes plus petites qui dépendent toujours des globaux / statiques.

Quelle serait la voie à suivre pour réaliser une telle migration progressive?

PS La question Approches progressives de l'injection de dépendance est similaire dans son titre, mais elle ne répond pas à ma question.

damluar
la source
1
Voulez-vous passer directement des classes couplées à l'utilisation d'un conteneur d'injection de dépendance? Vous devez d'abord effectuer un refactoring pour découpler les classes, en utilisant des interfaces avant même de penser à vous marier à n'importe quel framework ou outil d'injection de dépendances.
Tulains Córdova
C'est suffisant. J'ai donc une classe d'utilité A qui est utilisée dans 30 autres classes (dont l'une est la classe B), ce qui les rend non testables. J'ai pensé faire de la classe A une dépendance constructeur de la classe B. Je dois changer tous les appels aux constructeurs et passer A à l'intérieur. Mais d'où dois-je l'obtenir?
damluar
Si B est la plus petite unité à laquelle vous pouvez appliquer DI pour commencer, alors je ne vois aucun mal à construire A où que vous construisiez B pour le moment - à condition que A n'ait pas de dépendances. Une fois que le ballon roule, vous pouvez revenir le refactoriser.
Andy Hunt
@damluar Une usine pour commencer?
Tulains Córdova
1
@damluar une classe ne devient pas impossible à tester simplement parce qu'elle repose sur une classe utilitaire. Cela signifie simplement que votre unité est légèrement plus grande que vous ne le souhaitez. Si vous pouvez tester votre classe d'utilitaire par elle-même, vous pouvez l'utiliser sans souci dans les tests pour les 30 autres classes. Lisez le blog de Martin Fowlers sur les tests unitaires.
gbjbaanb

Réponses:

2

Désolé, C#c'est ma langue de choix, je peux lire, Javamais je ferais probablement disparaître la syntaxe en essayant de l'écrire ... Les mêmes concepts s'appliquent entre C#et Javabien, alors j'espère que cela montrera les étapes de la façon dont vous pouvez progressivement déplacer votre base de code pour être plus testable.

Donné:

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();
}

pourrait facilement être refactorisé pour utiliser DI, sans l'utilisation d'un conteneur IOC - et vous pourriez même le décomposer en plusieurs étapes:

(potentiel) Première étape - prendre en compte les dépendances mais pas de modification du code appelant (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();
}

Refactor 2 (ou premier refactor si l'implémentation du conteneur IOC et la modification immédiate du code d'appel):

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();
    }

}

L'étape 2 pourrait techniquement être effectuée seule - mais cela représenterait (potentiellement) beaucoup plus de travail - en fonction du nombre de classes qui "actualisent" actuellement la fonctionnalité que vous recherchez pour DI.

Pensez à suivre l'étape 1 -> étape 2 - vous seriez en mesure de créer des tests unitaires pour Foo, indépendamment de Bar. Alors qu'avant le refactor de l'étape 1, il n'était pas facile à réaliser sans utiliser l'implémentation réelle des deux classes. Faire l'étape 1 -> l'étape 2 (plutôt que l'étape 2 immédiatement) permettrait de plus petits changements au fil du temps, et vous aurez déjà votre début d'un harnais de test afin de mieux vous assurer que votre refactoriste a fonctionné sans conséquence.

Kritner
la source
0

Le concept est le même que vous utilisiez Java, PHP ou même C #. Cette question est assez bien traitée par Gemma Anible dans cette vidéo YouTube:

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

Vous remplacez le code non testable par «façade» (faute d'un meilleur terme) qui appelle un nouveau code testable. Ensuite, vous pouvez progressivement remplacer les anciens appels par les services injectés. Je l'ai fait dans le passé et cela fonctionne assez bien.

Kevin G.
la source