Ist Poor Man's Dependency Injection ein guter Weg, um Testbarkeit in eine Legacy-Anwendung einzuführen?

14

Im vergangenen Jahr habe ich ein neues System mit Dependency Injection und einem IOC-Container erstellt. Das hat mir viel über DI beigebracht!

Ich halte es jedoch auch nach dem Erlernen der Konzepte und Muster für eine Herausforderung, Code zu entkoppeln und einen IOC-Container in eine Legacy-Anwendung einzuführen. Die Anwendung ist so groß, dass eine echte Implementierung überwältigend wäre. Auch wenn der Wert verstanden und die Zeit gewährt wurde. Wer hat Zeit für so etwas?

Ziel ist es natürlich, Unit-Tests in die Geschäftslogik einzubinden!
Geschäftslogik, die mit testverhindernden Datenbankaufrufen verknüpft ist.

Ich habe die Artikel gelesen und verstehe die Gefahren von Poor Man's Dependency Injection, wie in diesem Artikel von Los Techies beschrieben . Ich verstehe, dass es nichts wirklich entkoppelt.
Ich verstehe, dass es viel systemweites Refactoring beinhalten kann, da Implementierungen neue Abhängigkeiten erfordern. Ich würde nicht in Betracht ziehen, es für ein neues Projekt mit beliebiger Größe zu verwenden.

Frage: Ist es in Ordnung, Poor Man's DI zu verwenden, um Testbarkeit in eine Legacy-Anwendung einzuführen und den Ball ins Rollen zu bringen?

Ist die Verwendung von Poor Man's DI als Basisansatz für echte Abhängigkeitsinjektion eine wertvolle Methode, um über die Notwendigkeit und den Nutzen des Prinzips zu informieren?

Können Sie eine Methode umgestalten, die eine Datenbankaufrufabhängigkeit und eine Zusammenfassung aufweist, die hinter einer Schnittstelle aufgerufen wird? Allein diese Abstraktion würde diese Methode testbar machen, da eine Scheinimplementierung über eine Konstruktorüberladung übergeben werden könnte.

Sobald die Bemühungen Befürworter gewinnen, könnte das Projekt aktualisiert werden, um einen IOC-Container zu implementieren, und die Konstrukteure, die die Abstraktionen aufnehmen, wären da draußen.

Airn5475
quelle
2
Nur eine Notiz. I consider it a challenge to decouple code and introduce an IOC container into a legacy applicationnatürlich ist es. Es heißt technische Schulden. Aus diesem Grund sind kleine und fortlaufende Refaktoren vor allen größeren Umbauten vorzuziehen. Die größten Konstruktionsfehler zu reduzieren und auf IoC umzusteigen, wäre weniger schwierig.
Laiv
Von der gleichen Stelle ist Abhängigkeitsinjektion nicht
Mason Wheeler

Antworten:

25

Die Kritik an Poor Man's Injection in NerdDinner hat weniger damit zu tun, ob Sie einen DI-Container verwenden oder nicht, als dass Sie Ihre Klassen korrekt einrichten .

In dem Artikel heißt es, dass

public class SearchController : Controller {

    IDinnerRepository dinnerRepository;

    public SearchController() : this(new DinnerRepository()) { }

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

ist falsch, da der erste Konstruktor zwar einen praktischen Fallback-Mechanismus zum Erstellen der Klasse bereitstellt, aber auch eine enge Abhängigkeit zu erstellt DinnerRepository.

Das richtige Mittel ist natürlich nicht, wie Los Techies vorschlägt, einen DI-Container hinzuzufügen, sondern den fehlerhaften Konstruktor zu entfernen.

public class SearchController : Controller 
{
    IDinnerRepository dinnerRepository;

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

Die Abhängigkeiten der verbleibenden Klasse werden nun ordnungsgemäß invertiert. Sie können diese Abhängigkeiten nun beliebig einfügen.

Robert Harvey
quelle
Danke für deine Gedanken! Ich verstehe den "beleidigenden Konstruktor" und wie wir es vermeiden sollten. Ich verstehe, dass diese Abhängigkeit nichts entkoppelt, aber Unit-Tests zulässt. Ich bin gerade dabei, DI einzuführen und Unit-Tests so schnell wie möglich einzuführen. Ich versuche, die Komplikation eines DI / IOC-Containers oder einer Fabrik so früh im Spiel zu vermeiden. Wie bereits erwähnt, könnten wir später, wenn die Unterstützung für DI wächst, einen Container implementieren und diese "störenden Konstruktoren" entfernen.
Airn5475
Der Standardkonstruktor ist für Komponententests nicht erforderlich. Sie können der Klasse jede Abhängigkeit übergeben, die Sie möchten, einschließlich eines Scheinobjekts.
Robert Harvey
2
@ Airn5475 vorsichtig sein , dass Sie nicht über zu abstrahieren. Diese Tests sollten aussagekräftige Verhaltensweisen testen - zu testen, ob ein XServiceParameter an einen übergeben XRepositoryund das zurückgegeben wird, was er zurückgibt, ist für niemanden übermäßig nützlich und bietet auch in Bezug auf die Erweiterbarkeit nicht viel. Schreiben Sie Ihre Komponententests, um ein aussagekräftiges Verhalten zu testen, und stellen Sie sicher, dass Ihre Komponenten nicht so eng sind, dass Sie Ihre gesamte Logik auf Ihre Kompositionswurzel verlagern.
Ant P
Ich verstehe nicht, wie das Einschließen des beleidigenden Konstruktors für Komponententests helfen würde, sicher tut es das Gegenteil? Entfernen Sie den anstößigen Konstruktor, damit die DI ordnungsgemäß implementiert wird, und übergeben Sie eine verspottete Abhängigkeit, wenn Sie die Objekte instanziieren.
Rob
@Rob: Das tut es nicht. Es ist nur eine bequeme Möglichkeit für den Standardkonstruktor, ein gültiges Objekt zu aktivieren.
Robert Harvey
16

Sie machen hier eine falsche Annahme darüber, was "der DI des armen Mannes" ist.

Das Erstellen einer Klasse mit einem "Shortcut" -Konstruktor, der immer noch eine Kopplung erzeugt, ist keine DI für den armen Mann.

Das Fehlen eines Behälters und das manuelle Erstellen aller Injektionen und Zuordnungen ist die DI des armen Mannes.

Der Begriff "DI des armen Mannes" klingt nach einer schlechten Sache. Aus diesem Grund wird der Begriff "reines DI" heutzutage empfohlen, da er positiver klingt und den Prozess genauer beschreibt.

Es ist nicht nur absolut in Ordnung, DIs / pure DIs von Armen zu verwenden, um DIs in eine vorhandene App einzuführen, sondern auch eine gültige Methode, DIs für viele neue Anwendungen zu verwenden. Und wie Sie sagen, sollte jeder an mindestens einem Projekt pure DI verwenden, um wirklich zu verstehen, wie DI funktioniert, bevor er die Verantwortung auf die "Magie" eines IoC-Containers überträgt.

David Arno
quelle
8
"Poor Man's" oder "Pure" DI, je nachdem, was Sie bevorzugen, lindert auch viele Probleme mit IoC-Containern. Früher habe ich für alles IoC-Container verwendet, aber je mehr Zeit vergeht, desto mehr ziehe ich es vor, sie insgesamt zu meiden.
Ant P
7
@AntP, ich bin bei dir: Ich bin zu 100% DI in diesen Tagen und vermeide aktiv Container. Aber ich (wir) sind in dieser Hinsicht in der Minderheit, soweit ich das beurteilen kann.
David Arno
2
Scheint so traurig zu sein - als ich mich vom "magischen" Lager der IoC-Container entferne, Bibliotheken und so weiter verspotte und OOP, Verkapselung, handgerollte Test-Doubles und Zustandsüberprüfung zu schätzen weiß, scheint der Trend für Magie immer noch im Gange zu sein oben. +1 sowieso.
Ant P
3
@AntP & David: Es ist schön zu hören, dass ich nicht alleine im 'Pure DI'-Camp bin. DI-Container wurden erstellt, um Sprachmängel zu beheben. Sie schaffen in diesem Sinne einen Mehrwert. Aber in meinen Augen ist die eigentliche Antwort, die Sprachen zu reparieren (oder zu anderen Sprachen zu wechseln) und keine Kruft darüber zu bauen.
JimmyJames
1

Paragidm-Verschiebungen in Legacy-Teams / Codebasen sind äußerst riskant:

Jedes Mal , wenn Sie vorschlagen „Verbesserungen“ zu Legacy - Code und es gibt „Legacy“ Programmierer im Team, Sie sagen nur jeden , dass „sie haben es falsch“ und Sie werden der Feind für den Rest Ihrer / ihre Zeit mit dem Unternehmen.

Wenn Sie ein DI-Framework als Hammer verwenden, um alle Nägel zu zertrümmern, wird Legacy-Code in allen Fällen eher schlechter als besser. Es ist auch persönlich äußerst riskant.

Selbst in den seltensten Fällen, wie zum Beispiel wenn es in Testfällen verwendet werden soll, werden die Testfälle zu "nicht-standardmäßigen" und "fremden" Codes, die bestenfalls markiert werden, @Ignorewenn sie beschädigt werden oder sich ständig über sie beschweren Legacy-Programmierer mit der größten Schlagkraft im Management und mit all dieser "verschwendeten Zeit für Komponententests", die nur Ihnen angelastet wird, werden "richtig" umgeschrieben.

Die Einführung eines DI-Frameworks oder sogar des Konzepts von "Pure DI" in einer Branchen-App, geschweige denn einer riesigen Legacy-Codebasis ohne die Zustimmung des Managements, des Teams und insbesondere des Sponsors des Lead-Entwicklers, wird der Todesstoß sein für Sie sozial und politisch im Team / Unternehmen. Solche Dinge zu tun ist extrem riskant und kann im schlimmsten Fall politischer Selbstmord sein.

Abhängigkeitsinjektion ist eine Lösung, die nach einem Problem sucht.

Jede Sprache mit Konstruktoren verwendet per Definition die Abhängigkeitsinjektion per Konvention. Wenn Sie wissen, was Sie tun und wissen, wie Sie einen Konstruktor richtig verwenden, ist dies nur ein gutes Design.

Die Abhängigkeitsinjektion ist nur in kleinen Dosen für einen sehr engen Bereich von Dingen nützlich:

  • Dinge, die sich stark ändern oder viele alternative Implementierungen haben, die statisch gebunden sind.

    • JDBC-Treiber sind ein perfektes Beispiel.
    • HTTP-Clients, die von Plattform zu Plattform unterschiedlich sein können.
    • Protokollierungssysteme, die je nach Plattform variieren.
  • Pluginsysteme mit konfigurierbaren Plugins, die in Ihrem Framework im Konfigurationscode definiert und beim Start automatisch erkannt und während der Ausführung des Programms dynamisch geladen / neu geladen werden können.


quelle
10
Die Killer-App für DI ist Unit-Test. Wie Sie bereits betont haben, wird die meiste Software von sich aus nicht viel DI benötigen. Tests sind jedoch auch ein Client für diesen Code, daher sollten wir die Testbarkeit berücksichtigen. Dies bedeutet insbesondere, dass Nähte in unserem Design so platziert werden, dass die Tests das Verhalten beobachten können. Dies erfordert kein ausgefallenes DI-Framework, aber es erfordert, dass relevante Abhängigkeiten explizit und ersetzbar gemacht werden.
amon
4
Wenn wir Entwickler wüssten, was sich ändern könnte, wäre das die halbe Miete . Ich habe lange an Entwicklungen gearbeitet, bei denen sich eine Menge vermeintlich "fester" Dinge geändert haben. Zukunftssicherheit hier und da mit DI ist keine schlechte Sache. Immer und überall zu verwenden, schmeckt nach Frachtkult-Programmierung.
Robbie Dee
Eine Überkomplizierung des Tests bedeutet lediglich, dass sie veraltet sind und in Zukunft als ignoriert und nicht mehr aktualisiert gekennzeichnet werden. DI in Legacy-Code ist eine falsche Ökonomie. Alles, was sie tun müssen, ist, Sie zum "Bösen" zu machen, um all diesen "nicht standardmäßigen, übermäßig komplexen Code, den niemand versteht" in die Codebasis aufzunehmen. Du wurdest gewarnt.
4
@JarrodRoberson, das ist wirklich eine giftige Umgebung. Eines der Hauptmerkmale eines guten Entwicklers ist, dass er auf den Code zurückblickt, den er in der Vergangenheit geschrieben hat, und denkt: "Hm, habe ich das wirklich geschrieben? Das ist so falsch!" Das liegt daran, dass sie als Entwickler gewachsen sind und neue Dinge gelernt haben. Jemand, der sich den Code ansieht, den er vor fünf Jahren geschrieben hat und an dem nichts auszusetzen ist, hat in diesen fünf Jahren nichts gelernt. Wenn Sie also den Leuten sagen, dass sie es in der Vergangenheit falsch gemacht haben, sind Sie ein Feind. Dann fliehen Sie schnell aus dieser Gesellschaft, denn sie sind ein Sackgasse-Team, das Sie zurückhält und Sie auf ihr schlechtes Niveau reduziert.
David Arno
1
Ich bin mir nicht sicher, ob ich mit den Bösewichten einverstanden bin, aber für mich ist die wichtigste Erkenntnis, dass Sie DI nicht benötigen, um Unit-Tests durchzuführen. Das Implementieren von DI, um den Code testbarer zu machen, ist nur eine Korrektur des Problems.
Ant P