Verschieben Sie die Codebasis schrittweise in den Abhängigkeitsinjektionscontainer

9

Ich habe eine große Codebasis mit vielen "Anti-Pattern" -Singletons, Dienstprogrammklassen mit statischen Methoden und Klassen, die ihre eigenen Abhängigkeiten mithilfe von newSchlüsselwörtern erstellen . Es macht es sehr schwierig, einen Code zu testen.

Ich möchte Code schrittweise in den Abhängigkeitsinjektionscontainer migrieren (in meinem Fall Guice, weil es sich um ein GWTProjekt handelt). Nach meinem Verständnis der Abhängigkeitsinjektion ist alles oder nichts. Entweder werden alle Klassen von Spring / Guice verwaltet oder keine. Da die Codebasis groß ist, kann ich den Code nicht über Nacht transformieren. Ich brauche also einen Weg, dies schrittweise zu tun.

Das Problem ist, dass ich, wenn ich mit einer Klasse beginne, die in andere Klassen eingefügt werden muss, @Injectin diesen Klassen keine einfache verwenden kann, da diese Klassen noch nicht vom Container verwaltet werden. Dies schafft also eine lange Kette bis zu den "Top" -Klassen, die nirgendwo injiziert werden.

Die einzige Möglichkeit, die ich sehe, besteht darin, einen Injector/ application-Kontext vorerst global über einen Singleton verfügbar zu machen , damit andere Klassen verwaltete Beans daraus erhalten können. Dies widerspricht jedoch der wichtigen Idee, das composition rootder Anwendung nicht preiszugeben .

Ein anderer Ansatz wäre Bottom-up: Beginnen Sie mit "High-Level" -Klassen, fügen Sie sie in den Abhängigkeitsinjektionscontainer ein und wechseln Sie langsam zu "kleineren" Klassen. Aber dann muss ich lange warten, da ich diese kleineren Klassen testen kann, die immer noch von Globals / Statics abhängen.

Was wäre der Weg, um eine solche schrittweise Migration zu erreichen?

PS Die Frage Allmähliche Ansätze zur Abhängigkeitsinjektion ist im Titel ähnlich, beantwortet aber meine Frage nicht.

Damluar
quelle
1
Möchten Sie direkt von fest gekoppelten Klassen zur Verwendung eines Abhängigkeitsinjektionscontainers übergehen? Sie müssen zunächst einige Umgestaltungen vornehmen, um Klassen mithilfe von Schnittstellen zu entkoppeln, bevor Sie überhaupt daran denken, ein Framework oder Tool für die Abhängigkeitsinjektion zu heiraten.
Tulains Córdova
Fair genug. Ich habe also eine Utility-Klasse A, die in 30 anderen Klassen verwendet wird (eine davon ist Klasse B), wodurch sie nicht mehr testbar sind. Ich dachte, Klasse A zu einer Konstruktorabhängigkeit von Klasse B zu machen. Ich muss alle Aufrufe an Konstruktoren ändern und A darin übergeben. Aber woher soll ich es bekommen?
Damluar
Wenn B die kleinste Einheit ist, auf die Sie DI anwenden können, um loszulegen, dann sehe ich keinen Schaden darin, A zu konstruieren, wo immer Sie B vorerst konstruieren - vorausgesetzt, A hat keine Abhängigkeiten. Sobald der Ball rollt, können Sie zurückkommen und ihn umgestalten.
Andy Hunt
@damluar Eine Fabrik für den Anfang?
Tulains Córdova
1
@damluar Eine Klasse wird nicht unprüfbar, nur weil sie auf einer Utility-Klasse basiert. Es bedeutet nur, dass Ihr Gerät etwas größer ist, als Sie es möchten. Wenn Sie Ihre Utility-Klasse selbst testen können, können Sie sie ohne Bedenken in Tests für alle anderen 30 Klassen verwenden. Lesen Sie den Blog von Martin Fowlers über Unit-Tests.
Gbjbaanb

Antworten:

2

Tut C#mir leid, ist meine Sprache der Wahl, ich kann lesen Java, würde aber wahrscheinlich die Syntax, die versucht, sie zu schreiben, abschlachten ... Die gleichen Konzepte gelten zwischen C#und Javaobwohl. Hoffentlich zeigt dies die Schritte, wie Sie Ihre Codebasis schrittweise verschieben können, um mehr zu werden testbar.

Gegeben:

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

könnte leicht umgestaltet werden, um DI ohne die Verwendung eines IOC-Containers zu verwenden - und Sie könnten es möglicherweise sogar in mehrere Schritte aufteilen:

(potenziell) Schritt eins - Abhängigkeiten berücksichtigen, aber keine Änderung am aufrufenden (UI) Code vornehmen:

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 (oder erster Refactor, wenn der IOC-Container implementiert und der Aufrufcode sofort geändert wird):

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

}

Schritt 2 könnte technisch von selbst erledigt werden - wäre aber (möglicherweise) viel mehr Arbeit - abhängig davon, wie viele Klassen derzeit die Funktionalität "neu" machen, die Sie für DI suchen.

Wenn Sie Schritt 1 -> Schritt 2 wählen, können Sie Foounabhängig davon Komponententests erstellen Bar. Während es vor dem Refactor von Schritt 1 nicht einfach war, ohne die tatsächliche Implementierung beider Klassen zu verwenden. Wenn Sie Schritt 1 -> Schritt 2 (und nicht sofort Schritt 2) ausführen, können Sie im Laufe der Zeit kleinere Änderungen vornehmen, und Sie haben bereits einen Testkabelbaum gestartet, um sicherzustellen, dass Ihr Refactor ohne Konsequenzen funktioniert.

Kritner
quelle
0

Das Konzept ist das gleiche, egal ob Sie Java, PHP oder sogar C # verwenden. Diese Frage wird von Gemma Anible in diesem YouTube-Video ziemlich gut behandelt:

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

Sie ersetzen den nicht testbaren Code durch "Fassade" (mangels eines besseren Begriffs), die neuen, testbaren Code aufruft. Dann können Sie nach und nach die älteren Anrufe durch die eingespeisten Dienste ersetzen. Ich habe das in der Vergangenheit gemacht und es funktioniert ziemlich gut.

Kevin G.
quelle