Abwägen der Abhängigkeitsinjektion mit öffentlichem API-Design

13

Ich habe darüber nachgedacht, wie man testbares Design mithilfe der Abhängigkeitsinjektion mit der Bereitstellung einer einfachen festen öffentlichen API ausbalanciert. Mein Dilemma ist: Die Leute möchten so etwas tun var server = new Server(){ ... }und müssen sich nicht darum kümmern, die vielen Abhängigkeiten und die Abhängigkeitsgraphen zu erstellen, die ein Mensch haben Server(,,,,,,)kann. Während der Entwicklung mache ich mir keine allzu großen Sorgen, da ich ein IoC / DI-Framework verwende, um all das zu handhaben (ich verwende nicht die Aspekte des Lebenszyklusmanagements eines Containers, die die Dinge weiter komplizieren würden).

Jetzt ist es unwahrscheinlich, dass die Abhängigkeiten erneut implementiert werden. In diesem Fall dient die Komponentierung fast ausschließlich der Testbarkeit (und einem anständigen Design!), Anstatt Nähte für Erweiterungen usw. zu erstellen. In 99,999% der Fälle möchten Benutzer eine Standardkonfiguration verwenden. So. Ich könnte die Abhängigkeiten fest codieren. Will das nicht, wir verlieren unsere Tests! Ich könnte einen Standardkonstruktor mit fest codierten Abhängigkeiten und einen, der Abhängigkeiten akzeptiert, bereitstellen. Das ist ... chaotisch und wahrscheinlich verwirrend, aber machbar. Ich könnte den abhängigkeitsempfangenden Konstruktor intern machen und meine Unit-Tests zu einer Friend-Assembly machen (unter der Annahme von C #), die die öffentliche API aufräumt, aber eine unangenehme versteckte Falle zur Wartung zurücklässt. Zwei Konstruktoren zu haben, die implizit und nicht explizit miteinander verbunden sind, wäre in meinem Buch im Allgemeinen ein schlechtes Design.

Im Moment ist das ungefähr das geringste Übel, an das ich denken kann. Meinungen? Weisheit?

kolektiv
quelle

Antworten:

11

Die Abhängigkeitsinjektion ist ein starkes Muster, wenn sie gut angewendet wird, aber allzu oft sind ihre Praktiker von einem externen Rahmen abhängig. Die daraus resultierende API ist sehr schmerzhaft für diejenigen von uns, die unsere App nicht lose mit XML-Klebeband zusammenbinden möchten. Vergessen Sie nicht über Plain Old Objects (POO).

Überlegen Sie zunächst, wie Sie von jemandem erwarten würden, dass er Ihre API verwendet - ohne das dazugehörige Framework.

  • Wie instanziieren Sie den Server?
  • Wie erweitern Sie den Server?
  • Was möchten Sie in der externen API verfügbar machen?
  • Was möchten Sie in der externen API verbergen ?

Mir gefällt die Idee, Standardimplementierungen für Ihre Komponenten bereitzustellen, und die Tatsache, dass Sie über diese Komponenten verfügen. Der folgende Ansatz ist eine absolut gültige und anständige OO-Praxis:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Solange Sie die Standardimplementierungen für die Komponenten in den API-Dokumenten dokumentieren und einen Konstruktor bereitstellen, der das gesamte Setup ausführt, sollten Sie in Ordnung sein. Die kaskadierenden Konstruktoren sind nicht zerbrechlich, solange der Einrichtungscode in einem Master-Konstruktor vorkommt.

Grundsätzlich verfügen alle öffentlich zugänglichen Konstruktoren über die verschiedenen Kombinationen von Komponenten, die Sie verfügbar machen möchten. Intern würden sie die Standardeinstellungen auffüllen und an einen privaten Konstruktor übergeben, der die Einrichtung abschließt. Im Wesentlichen liefert dies etwas TROCKEN und ist ziemlich einfach zu testen.

Wenn Sie friendZugriff auf Ihre Tests gewähren müssen, um das Server-Objekt so einzurichten, dass es zu Testzwecken isoliert wird , greifen Sie zu. Es wird nicht Teil der öffentlichen API sein, mit der Sie vorsichtig sein möchten.

Seien Sie freundlich zu Ihren API Verbraucher und nicht benötigen einen IoC / DI Rahmen Ihrer Bibliothek zu verwenden. Um ein Gefühl für die Schmerzen zu bekommen, die Sie verursachen, stellen Sie sicher, dass Ihre Unit-Tests auch nicht auf dem IoC / DI-Framework basieren. (Es ist ein persönlicher Pet Peeve, aber sobald Sie das Framework einführen, werden keine Unit-Tests mehr durchgeführt - es werden Integrationstests durchgeführt).

Berin Loritsch
quelle
Ich bin damit einverstanden, und ich bin sicherlich kein Fan von IoC-Konfigurationssuppe - XML-Konfiguration verwende ich überhaupt nicht, wenn es um IoC geht. Das Erfordernis der Verwendung von IoC ist genau das, was ich vermieden habe - es ist die Art von Annahme, die ein öffentlicher API-Designer niemals machen kann. Vielen Dank für den Kommentar, genau so, wie ich es mir vorgestellt habe, und es ist beruhigend, hin und wieder eine Überprüfung der geistigen Gesundheit durchführen zu lassen!
Kolektiv
-1 Diese Antwort besagt im Wesentlichen "Keine Abhängigkeitsinjektion verwenden" und enthält ungenaue Annahmen. Wenn sich in Ihrem Beispiel der HttpListenerKonstruktor ändert, Servermuss sich auch die Klasse ändern. Sie sind gekoppelt. Eine bessere Lösung ist das, was @Winston Ewert weiter unten sagt, nämlich die Bereitstellung einer Standardklasse mit vordefinierten Abhängigkeiten außerhalb der API der Bibliothek. Dann ist der Programmierer nicht gezwungen, alle Abhängigkeiten einzurichten, da Sie die Entscheidung für ihn treffen, aber er hat immer noch die Flexibilität, sie später zu ändern. Dies ist eine große Sache, wenn Sie Abhängigkeiten von Drittanbietern haben.
M. Dudley
FWIW das zur Verfügung gestellte Beispiel heißt "Bastardinjektion". stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley
1
@ MichaelDudley, hast du die Frage des OP gelesen? Alle "Annahmen" basierten auf den vom OP bereitgestellten Informationen. Eine Zeit lang war die "Bastard-Injection", wie Sie sie nannten, das bevorzugte Dependency-Injection-Format - insbesondere für Systeme, deren Zusammensetzung sich zur Laufzeit nicht ändert. Setter und Getter sind auch eine andere Form der Abhängigkeitsinjektion. Ich habe einfach Beispiele verwendet, basierend auf dem, was das OP lieferte.
Berin Loritsch
4

In Java ist es in solchen Fällen üblich, dass eine "Standard" -Konfiguration von einer Fabrik erstellt wird, die unabhängig von dem tatsächlichen "ordnungsgemäß" verhaltenen Server ist. Ihr Benutzer schreibt also:

var server = DefaultServer.create();

Der ServerKonstruktor des Moduls akzeptiert weiterhin alle Abhängigkeiten und kann für eine umfassende Anpassung verwendet werden.

fdreger
quelle
+1, SRP. Die Aufgabe einer Zahnarztpraxis besteht nicht darin, eine Zahnarztpraxis zu errichten. Die Verantwortung eines Servers besteht nicht darin, einen Server zu erstellen.
R. Schmitz
2

Wie wäre es mit einer Unterklasse, die die "öffentliche API" liefert?

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

Der Benutzer kannnew StandardServer() und ist unterwegs. Sie können auch die Server-Basisklasse verwenden, wenn Sie mehr Kontrolle über die Funktionsweise des Servers wünschen. Dies ist mehr oder weniger der Ansatz, den ich benutze. (Ich verwende kein Framework, da ich den Punkt noch nicht gesehen habe.)

Dies macht die interne API immer noch verfügbar, aber ich denke, Sie sollten dies tun. Sie haben Ihre Objekte in verschiedene nützliche Komponenten aufgeteilt, die unabhängig voneinander funktionieren sollen. Es ist nicht abzusehen, wann ein Dritter eine der Komponenten isoliert verwenden möchte.

Winston Ewert
quelle
1

Ich könnte den abhängigkeitsempfangenden Konstruktor intern machen und meine Unit-Tests zu einer Friend-Assembly machen (unter der Annahme von C #), die die öffentliche API aufräumt, aber eine unangenehme versteckte Falle zur Wartung zurücklässt.

Das scheint mir keine "böse versteckte Falle" zu sein. Der öffentliche Konstruktor sollte nur den internen mit den "Standard" -Abhängigkeiten aufrufen. Solange klar ist, dass der öffentliche Konstruktor nicht geändert werden sollte, sollte alles in Ordnung sein.

Anon.
quelle
0

Ich stimme Ihrer Meinung voll und ganz zu. Wir möchten die Grenzen der Komponentennutzung nicht nur zum Zweck des Komponententests verschmutzen. Dies ist das gesamte Problem der DI-basierten Testlösung.

Ich würde vorschlagen, InjectableFactory / InjectableInstance- Muster zu verwenden. InjectableInstance sind einfache wiederverwendbare Dienstprogrammklassen, die einen veränderlichen Wert besitzen . Die Komponentenschnittstelle enthält einen Verweis auf diesen Werthalter, der mit der Standardimplementierung initialisiert wurde. Die Schnittstelle stellt eine Singleton-Methode get () bereit , die an den Werthalter delegiert. Der Komponentenclient ruft die Methode get () anstelle von new auf, um eine Implementierungsinstanz abzurufen und direkte Abhängigkeiten zu vermeiden. Während der Testzeit können wir optional die Standardimplementierung durch einen Schein ersetzen. Im Folgenden werde ich ein Beispiel TimeProvider verwendenKlasse, die eine Abstraktion von Date () ist, sodass Unit-Tests injizieren und verspotten können, um verschiedene Momente zu simulieren. Entschuldigung, ich werde Java hier verwenden, da ich mit Java besser vertraut bin. C # sollte ähnlich sein.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

Die InjectableInstance ist ziemlich einfach zu implementieren, ich habe eine Referenzimplementierung in Java. Weitere Informationen finden Sie in meinem Blogbeitrag Abhängigkeitsrichtlinie mit Injectable Factory

Jianwu Chen
quelle
Also ... Sie ersetzen die Abhängigkeitsinjektion durch einen veränderlichen Singleton? Das klingt schrecklich, wie würden Sie unabhängige Tests durchführen? Öffnen Sie für jeden Test einen neuen Prozess oder erzwingen Sie eine bestimmte Reihenfolge? Java ist nicht meine Stärke, habe ich etwas falsch verstanden?
Nvoigt
In Java Maven-Projekten ist die gemeinsam genutzte Instanz kein Problem, da die Junit-Engine standardmäßig jeden Test in einem anderen Klassenladeprogramm ausführt. Dieses Problem tritt jedoch in einigen IDE-Umgebungen auf, da möglicherweise dasselbe Klassenladeprogramm wiederverwendet wird. Um dieses Problem zu lösen, stellt die Klasse eine Rücksetzmethode bereit, die aufgerufen werden kann, um nach jedem Test in den ursprünglichen Zustand zurückzukehren. In der Praxis ist es eine seltene Situation, dass unterschiedliche Tests unterschiedliche Mocks verwenden möchten. Wir wenden dieses Muster seit Jahren für Großprojekte an. Alles läuft reibungslos, wir haben lesbaren, sauberen Code genossen, ohne auf Portabilität und Testbarkeit verzichten zu müssen.
Jianwu Chen