Wann werden Schnittstellen verwendet (Komponententest, IoC?)

17

Ich vermute, ich habe hier einen Schülerfehler gemacht und suche nach einer Klärung. Viele der Klassen in meiner Lösung (C #) - ich wage es, die Mehrheit zu sagen - haben am Ende eine entsprechende Schnittstelle für geschrieben. ZB eine "ICalculator" -Schnittstelle und eine "Calculator" -Klasse, die sie implementiert, obwohl ich diesen Rechner wahrscheinlich nie durch eine andere Implementierung ersetzen werde. Außerdem befinden sich die meisten dieser Klassen im selben Projekt wie ihre Abhängigkeiten - sie müssen es wirklich nur sein internal, sind aber letztendlich publicein Nebeneffekt bei der Implementierung ihrer jeweiligen Schnittstellen.

Ich denke, diese Praxis, Schnittstellen für alles zu schaffen, ist auf ein paar Lügen zurückzuführen:

1) Ich dachte ursprünglich, dass eine Schnittstelle notwendig ist, um Unit-Test-Mocks zu erstellen (ich verwende Moq), aber seitdem habe ich herausgefunden, dass eine Klasse verspottet werden kann, wenn ihre Mitglieder sind virtual, und dass sie einen parameterlosen Konstruktor hat (korrigieren Sie mich, wenn Ich liege falsch).

2) Ich dachte ursprünglich, dass eine Schnittstelle notwendig ist, um eine Klasse beim IoC-Framework (Castle Windsor) zu registrieren, z

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

in der Tat konnte ich nur den konkreten Typ gegen sich selbst registrieren:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) Die Verwendung von Schnittstellen, z. B. Konstruktorparametern für die Abhängigkeitsinjektion, führt zu einer "losen Kopplung".

Also bin ich verrückt nach Schnittstellen geworden ?! Mir sind die Szenarien bekannt, in denen Sie "normalerweise" eine Schnittstelle verwenden würden, z. B. um eine öffentliche API verfügbar zu machen, oder für Dinge wie "steckbare" Funktionalität. Meine Lösung verfügt über eine kleine Anzahl von Klassen, die für solche Anwendungsfälle geeignet sind. Ich frage mich jedoch, ob alle anderen Schnittstellen unnötig sind und entfernt werden sollten. Verstoße ich in Bezug auf Punkt 3) nicht gegen "lose Kopplung", wenn ich dies tun würde?

Bearbeiten : - Ich habe gerade ein Spiel mit Moq, und es scheint, dass Methoden öffentlich und virtuell sein und einen öffentlichen parameterlosen Konstruktor haben müssen, um sie verspotten zu können. Es sieht also so aus, als ob ich keine internen Klassen haben kann?

Andrew Stephens
quelle
Ich bin mir ziemlich sicher, dass Sie in C # keine Klasse veröffentlichen müssen, um eine Schnittstelle zu implementieren, auch wenn die Schnittstelle öffentlich ist ...
vaughandroid
1
Du sollst keine privaten Mitglieder testen . Dies kann als Indikator für das Anti-Muster der Götterklasse angesehen werden. Wenn Sie das Bedürfnis haben, private Funktionen zu testen, ist es normalerweise eine gute Idee, diese Funktionen in einer eigenen Klasse zu kapseln.
Spoike
@Spoike das fragliche Projekt ist ein UI-Projekt, in dem es natürlich schien, alle Klassen intern zu machen, aber sie müssen noch Unit-Tests? Ich habe an anderer Stelle gelesen, dass man interne Methoden nicht testen muss, aber die Gründe dafür habe ich nie verstanden.
Andrew Stephens
Fragen Sie sich zuerst, welches Gerät getestet wird. Ist es die ganze Klasse oder eine Methode? Dieses Gerät muss öffentlich sein, damit es ordnungsgemäß getestet werden kann. In UI-Projekten trenne ich normalerweise testbare Logik in Controller / Ansichtsmodelle / Dienste, die alle öffentliche Methoden haben. Die aktuellen Ansichten / Widgets sind mit den Controllern / Ansichtsmodellen / Diensten verbunden und werden durch regelmäßige Rauchtests getestet (dh starten Sie die App und klicken Sie darauf). Sie können Szenarien haben, in denen die zugrunde liegende Funktionalität getestet werden sollte, und ein Komponententest für die Widgets führt zu fragilen Tests und sollte mit Vorsicht durchgeführt werden.
Spoike
@Spoike, machen Sie Ihre VM-Methoden öffentlich, um sie für unsere Komponententests zugänglich zu machen? Vielleicht irre ich mich dorthin und versuche, alles intern zu machen. Ich denke, dass meine Hände mit Moq verbunden sein könnten, was zu erfordern scheint, dass Klassen (nicht nur Methoden) öffentlich sind, um sie zu verspotten (siehe meinen Kommentar zu Telastyns Antwort).
Andrew Stephens

Antworten:

7

Wenn Sie eine Schnittstelle mit nur einem Implementierer erstellen, schreiben Sie die Dinge im Allgemeinen nur zweimal und verschwenden Ihre Zeit. Schnittstellen alleine bieten keine lose Kopplung, wenn sie eng mit einer Implementierung verbunden sind. Wenn Sie diese Dinge in Komponententests verspotten möchten, ist dies in der Regel ein gutes Zeichen dafür, dass Sie in der Realität zwangsläufig mehr als einen Implementierer benötigen Code.

Ich würde es für einen Codegeruch halten, wenn fast alle Klassen Schnittstellen haben. Das heißt, sie arbeiten fast alle auf die eine oder andere Weise miteinander. Ich würde vermuten, dass die Klassen zu viel tun (da es keine Hilfsklassen gibt) oder Sie zu viel abstrahiert haben (oh, ich möchte eine Provider-Schnittstelle für die Schwerkraft, da sich das ändern könnte!).

Telastyn
quelle
1
Danke für die Antwort. Wie bereits erwähnt, gibt es einige falsche Gründe, warum ich das getan habe, was ich getan habe, obwohl ich wusste, dass die meisten Schnittstellen niemals mehr als eine Implementierung haben würden. Ein Teil des Problems besteht darin, dass ich das Moq-Framework verwende, das sich hervorragend zum Verspotten von Interfaces eignet. Um eine Klasse zu verspotten, muss sie öffentlich sein, eine öffentliche parameterlose CTR haben und die zu verspottenden Methoden müssen 'public virtual' sein.
Andrew Stephens
Die meisten Klassen, für die ich Unit-Tests durchführe, sind interne Klassen und ich möchte sie nicht öffentlich machen, nur um sie testbar zu machen (obwohl Sie argumentieren könnten, habe ich all diese Schnittstellen dafür geschrieben). Wenn ich meine Interfaces los werde, muss ich wahrscheinlich ein neues Mocking Framework finden!
Andrew Stephens
1
@ AndrewStephens - nicht alles auf der Welt muss verspottet werden. Wenn die Klassen solide (oder eng gekoppelt) genug sind, um keine Schnittstellen zu benötigen, sind sie solide genug, um beim Unit-Test nur den Ist-Zustand zu verwenden. Die Zeit / Komplexität, die sie benötigen, ist die Testvorteile nicht wert.
Telastyn
-1, da Sie oft nicht wissen, wie viele Implementierer Sie benötigen, wenn Sie den Code zum ersten Mal schreiben. Wenn Sie von Anfang an eine Schnittstelle verwenden, müssen Sie beim Schreiben zusätzlicher Implementierer die Clients nicht ändern.
Wilbert
1
@wilbert - sicher, eine kleine Schnittstelle ist lesbarer als die Klasse, aber Sie wählen nicht die eine oder andere aus, Sie haben entweder die Klasse oder eine Klasse und eine Schnittstelle. Vielleicht lesen Sie zu viel in meiner Antwort. Ich sage nicht, dass Sie niemals Schnittstellen für eine einzelne Implementierung erstellen sollten - die meisten sollten dies in der Tat tun, da die meisten Interaktionen diese Flexibilität erfordern sollten. Aber lies die Frage noch einmal. Eine Schnittstelle für alles zu schaffen, ist nur Frachtkult. IoC ist kein Grund. Mocks sind kein Grund. Die Anforderungen sind ein Grund, diese Flexibilität umzusetzen.
Telastyn
4

1) Auch wenn konkrete Klassen verspottbar sind, müssen Sie dennoch Member erstellen virtualund einen parameterlosen Konstruktor bereitstellen, der möglicherweise auffällig und unerwünscht ist. Sie (oder neue Teammitglieder) werden bald virtualin jeder neuen Klasse systematisch Konstruktoren und parameterlose Konstruktoren hinzufügen , ohne sich Gedanken zu machen, nur weil "so funktioniert".

Eine Schnittstelle ist IMO eine viel bessere Idee, wenn Sie eine Abhängigkeit verspotten müssen, da Sie die Implementierung so lange verschieben können, bis Sie sie wirklich benötigen, und einen schönen, klaren Vertrag über die Abhängigkeit abschließen können.

3) Wie ist das eine Lüge?

Ich glaube, Schnittstellen eignen sich hervorragend zum Definieren von Messaging-Protokollen zwischen zusammenarbeitenden Objekten. Es kann Fälle geben, in denen ihre Notwendigkeit in Frage gestellt wird, wie z. B. bei Domänenentitäten . Überall dort, wo Sie einen stabilen Partner haben (dh eine injizierte Abhängigkeit im Gegensatz zu einer transienten Referenz), mit der Sie kommunizieren müssen, sollten Sie im Allgemeinen die Verwendung einer Schnittstelle in Betracht ziehen.

guillaume31
quelle
3

Die Idee von IoC ist es, die Betontypen austauschbar zu machen. Selbst wenn Sie (im Moment) nur einen Rechner haben, der ICalculator implementiert, kann eine zweite Implementierung nur von der Schnittstelle und nicht von den Implementierungsdetails von Calculator abhängen.

Daher ist die erste Version Ihrer IoC-Container-Registrierung die richtige und die, die Sie in Zukunft verwenden sollten.

Wenn Sie Ihre Klassen öffentlich oder intern machen, ist das nicht wirklich verwandt, und das Gleiche gilt für das Moqing.

Wilbert
quelle
2

Ich würde mir wirklich mehr Sorgen darüber machen, dass Sie anscheinend das Bedürfnis verspüren, nahezu jeden Typ in Ihrem gesamten Projekt zu "verspotten".

Test-Doubles sind vor allem nützlich, um Ihren Code von der Außenwelt (Drittanbieter-Bibliotheken, Festplatten-E / A, Netzwerk-E / A usw.) oder von wirklich teuren Berechnungen zu isolieren. Wenn Sie mit Ihrem Isolations-Framework zu liberal sind (brauchen Sie es sogar WIRKLICH? Ist Ihr Projekt so komplex?), Kann dies leicht zu Tests führen, die an die Implementierung gekoppelt sind, und Ihr Design wird starrer. Wenn Sie eine Reihe von Tests schreiben, die überprüfen, ob eine Klasse die Methoden X und Y in dieser Reihenfolge aufgerufen und dann die Parameter a, b und c an die Methode Z übergeben hat, erhalten Sie dann WIRKLICH einen Wert? Manchmal erhalten Sie solche Tests, wenn Sie die Protokollierung oder Interaktion mit Bibliotheken von Drittanbietern testen. Dies sollte jedoch die Ausnahme und nicht die Regel sein.

Sobald Ihnen Ihr Isolations-Framework sagt, dass Sie etwas veröffentlichen müssen und dass Sie immer parameterlose Konstruktoren haben müssen, ist es an der Zeit, dem Teufel aus dem Weg zu gehen, da Ihr Framework Sie an diesem Punkt zwingt, schlechten (schlechteren) Code zu schreiben.

Ich spreche genauer über Schnittstellen und finde, dass sie in einigen Schlüsselfällen nützlich sind:

  • Wenn Sie Methodenaufrufe an verschiedene Typen senden müssen, z. B. wenn Sie mehrere Implementierungen haben (dies liegt auf der Hand, da die Definition einer Schnittstelle in den meisten Sprachen nur eine Sammlung virtueller Methoden ist).

  • Wenn Sie in der Lage sein müssen, den Rest Ihres Codes von einer Klasse zu isolieren. Dies ist die übliche Methode, um das Dateisystem, den Netzwerkzugriff, die Datenbank usw. von der Kernlogik Ihrer Anwendung zu trennen.

  • Wenn Sie die Steuerung umkehren müssen, um Anwendungsgrenzen zu schützen. Dies ist eine der leistungsstärksten Möglichkeiten zum Verwalten von Abhängigkeiten. Wir alle wissen, dass High-Level-Module und Low-Level-Module nichts voneinander wissen sollten, da sie sich aus unterschiedlichen Gründen ändern und Sie KEINE Abhängigkeiten von HttpContext in Ihrem Domain-Modell oder einem PDF-Rendering-Framework in Ihrer Matrix-Multiplikationsbibliothek haben möchten . Das Implementieren von Schnittstellen durch Grenzflächenklassen (auch wenn sie niemals ersetzt werden) erleichtert die Kopplung zwischen Ebenen und verringert das Risiko, dass Abhängigkeiten von Typen, die zu viele andere Typen kennen, durch die Ebenen sickern, drastisch.

Sara
quelle
1

Ich neige zu der Idee, dass, wenn eine Logik privat ist, die Implementierung dieser Logik für niemanden von Belang sein und als solche nicht getestet werden sollte. Wenn eine Logik so komplex ist, dass Sie sie testen möchten, hat diese Logik wahrscheinlich eine bestimmte Rolle und sollte daher öffentlich sein. Wenn ich z. B. Code mit einer privaten Hilfsmethode extrahiere, finde ich, dass dies normalerweise genauso gut in einer separaten öffentlichen Klasse (einem Formatierer, einem Parser, einem Mapper usw.) möglich ist.
Was die Schnittstellen angeht, lasse ich mich von der Notwendigkeit ablenken oder verspotten zu müssen. Sachen wie Repos möchte ich normalerweise stumm schalten oder verspotten können. Ein Mapper in einem Integrationstest (meistens) nicht so sehr.

Stefan Billiet
quelle