Ist es empfehlenswert, die Schnittstelle in derselben Datei wie die Basisklasse zu deklarieren?

29

Um austauschbar und testbar zu sein, müssen Dienste mit Logik normalerweise eine Schnittstelle haben, z

public class FooService: IFooService 
{ ... }

In Bezug auf das Design stimme ich dem zu, aber eines der Dinge, die mich bei diesem Ansatz stören, ist, dass Sie für einen Service zwei Dinge deklarieren müssen (die Klasse und die Schnittstelle) und in unserem Team normalerweise zwei Dateien (eine) für die Klasse und eine für die Schnittstelle). Ein weiteres Problem ist die Schwierigkeit bei der Navigation, da die Verwendung von "Gehe zu Definition" in IDE (VS2010) auf die Schnittstelle verweist (da sich andere Klassen auf die Schnittstelle beziehen), nicht auf die tatsächliche Klasse.

Ich dachte, dass das Schreiben von IFooService in die gleiche Datei wie FooService die oben genannte Verrücktheit reduzieren wird. Immerhin sind IFooService und FooService sehr verwandt. Ist das eine gute Übung? Gibt es einen guten Grund, warum sich IFooService in einer eigenen Datei befinden muss?

Louis Rhys
quelle
3
Wenn Sie immer nur eine Implementierung einer bestimmten Schnittstelle haben, ist die Schnittstelle nicht unbedingt erforderlich. Warum nicht die Klasse direkt nutzen?
Joris Timmermans
11
@MadKeithV für lose Kopplung und Testbarkeit?
Louis Rhys
10
@MadKeithV: du hast recht. Aber wenn Sie Unit - Tests für Code schreiben, der auf IFooService beruht, werden Sie in der Regel eine MockFooService schaffen, das ist eine zweite Implementierung dieser Schnittstelle.
Doc Brown
5
@DocBrown - Wenn Sie mehrere Implementierungen haben, verschwindet natürlich der Kommentar, aber auch der Nutzen, direkt zur "einzelnen Implementierung" zu gelangen (da es mindestens zwei gibt). Dann stellt sich die Frage: Welche Informationen befinden sich in der konkreten Implementierung, die an dem Punkt, an dem Sie die Schnittstelle verwenden, nicht bereits in der Schnittstellenbeschreibung / -dokumentation enthalten sein sollten?
Joris Timmermans
1
Offensichtliche Eigenwerbung: stackoverflow.com/questions/5840219/…
Marjan Venema

Antworten:

24

Es muss nicht in einer eigenen Datei sein, aber Ihr Team sollte sich für einen Standard entscheiden und sich daran halten.

Sie haben auch Recht, dass Sie mit "Gehe zur Definition" zur Benutzeroberfläche gelangen. Wenn Sie jedoch Resharper installiert haben, ist es nur ein Klick, um eine Liste der von dieser Benutzeroberfläche abgeleiteten Klassen / Benutzeroberflächen zu öffnen. Deshalb behalte ich die Schnittstelle in einer separaten Datei.

 

Scott Whitlock
quelle
5
Ich stimme dieser Antwort zu, schließlich sollte sich die IDE an unser Design anpassen und nicht umgekehrt, oder?
Marktani
1
Auch ohne Resharper machen F12 und Shift + F12 so ziemlich dasselbe. Nicht einmal einen Rechtsklick entfernt :) (um fair zu sein, werden Sie nur die direkte Verwendung der Benutzeroberfläche benötigen, nicht sicher, was die Resharper-Version tut)
Daniel B
@DanielB: In Resharper wechselt Alt-End zur Implementierung (oder zeigt eine Liste der möglichen Implementierungen an, zu denen Sie wechseln können).
StriplingWarrior
@StriplingWarrior dann scheint es genau das zu sein, was Vanille VS auch tut. F12 = gehe zur Definition, Shift + F12 = gehe zur Implementierung (Ich bin mir ziemlich sicher, dass dies ohne Resharper funktioniert, obwohl ich es installiert habe, es ist also schwierig zu bestätigen). Es ist eine der nützlichsten Verknüpfungen für die Navigation. Ich sage es nur.
Daniel B
@DanielB: Die Standardeinstellung für Umschalt-F12 lautet "Verwendungen suchen". jetbrains.com/resharper/webhelp/…
StriplingWarrior
15

Ich denke, Sie sollten sie getrennt aufbewahren. Wie Sie sagten, besteht die Idee darin, testbar und austauschbar zu bleiben. Indem Sie die Schnittstelle in derselben Datei wie Ihre Implementierung ablegen, verknüpfen Sie die Schnittstelle mit der spezifischen Implementierung. Sollten Sie sich entscheiden, ein Scheinobjekt oder eine andere Implementierung zu erstellen, wird die Schnittstelle nicht logisch von FooService getrennt.

Brad S
quelle
10

Laut SOLID sollte die Schnittstelle nicht nur in einer anderen Datei, sondern auch in einer anderen Assembly erstellt werden.

Warum? Denn jede Änderung an einer Quelldatei, die in eine Assembly kompiliert wird, erfordert eine Neukompilierung der Assembly, und jede Änderung an einer Assembly erfordert eine Neukompilierung einer abhängigen Assembly. Wenn Sie also basierend auf SOLID eine Implementierung A durch eine Implementierung B ersetzen möchten, während die Klasse C von der Schnittstelle abhängt, muss ich den Unterschied nicht kennen, und Sie müssen sicherstellen, dass die Assembly mit I erstellt wird Daran ändert sich nichts, wodurch die Verwendungen geschützt werden.

"Aber es ist nur eine Neukompilierung", höre ich Sie protestieren. Das mag sein, aber in Ihrer Smartphone-App, die die Datenbandbreite Ihrer Benutzer schont. Laden Sie eine Binärdatei herunter, die sich geändert hat, oder laden Sie diese Binärdatei und fünf andere herunter, deren Code davon abhängt? Nicht jedes Programm ist so geschrieben, dass es von Desktop-Computern in einem LAN verwendet werden kann. Sogar in diesem Fall, in dem Bandbreite und Speicher günstig sind, können kleinere Patch-Versionen von Nutzen sein, da sie über Active Directory oder ähnliche Domänenverwaltungsebenen problemlos in das gesamte LAN übertragen werden können. Ihre Benutzer werden nur einige Sekunden auf die Anwendung warten, wenn sie sich das nächste Mal anmelden, anstatt ein paar Minuten, damit das Ganze erneut installiert werden kann. Ganz zu schweigen davon, dass je weniger Assemblys beim Erstellen eines Projekts neu kompiliert werden müssen, desto schneller erfolgt die Erstellung.

Nun zum Haftungsausschluss: Dies ist nicht immer möglich oder machbar. Am einfachsten ist es, ein zentrales "Schnittstellen" -Projekt zu erstellen. Dies hat seine eigenen Nachteile; Code wird weniger wiederverwendbar, da das Schnittstellenprojekt UND das Implementierungsprojekt in anderen Apps referenziert werden müssen, wobei die Persistenzschicht oder andere Schlüsselkomponenten Ihrer App wiederverwendet werden. Sie können dieses Problem überwinden, indem Sie die Schnittstellen in enger gekoppelte Assemblys aufteilen. Dann haben Sie jedoch mehr Projekte in Ihrer App, was einen vollständigen Build sehr schmerzhaft macht. Der Schlüssel ist das Gleichgewicht und die Beibehaltung des locker gekoppelten Designs. Normalerweise können Sie Dateien nach Bedarf verschieben. Wenn Sie also feststellen, dass eine Klasse viele Änderungen benötigt, oder dass regelmäßig neue Implementierungen einer Schnittstelle erforderlich sind (möglicherweise zur Verbindung mit neu unterstützten Versionen anderer Software),

KeithS
quelle
8

Warum ist es unangenehm, separate Dateien zu haben? Für mich ist es viel ordentlicher und sauberer. Es ist üblich, einen Unterordner mit dem Namen "Interfaces" zu erstellen und Ihre IFooServer.cs-Dateien dort abzulegen, wenn Sie weniger Dateien in Ihrem Projektmappen-Explorer anzeigen möchten.

Der Grund, warum die Schnittstelle in einer eigenen Datei definiert ist, ist derselbe, warum Klassen normalerweise in einer eigenen Datei definiert sind: Die Projektverwaltung ist einfacher, wenn Ihre logische Struktur und Dateistruktur identisch sind, sodass Sie immer wissen, welche Datei eine bestimmte Klasse definiert Dies kann Ihnen das Leben beim Debuggen erleichtern (Ausnahmestapel-Traces geben normalerweise Datei- und Zeilennummern an) oder beim Zusammenführen von Quellcode in einem Quellcodeverwaltungs-Repository.

Avner Shahar-Kashtan
quelle
6

Es ist oft eine gute Praxis, Ihre Codedateien nur eine einzelne Klasse oder eine einzelne Schnittstelle enthalten zu lassen. Diese Codierungspraktiken sind jedoch ein Mittel zum Zweck - um Ihren Code besser zu strukturieren und die Arbeit damit zu erleichtern. Wenn Sie und Ihr Team es einfacher finden, mit ihnen zu arbeiten, wenn die Klassen mit ihren Schnittstellen zusammengehalten werden, tun Sie dies auf jeden Fall.

Persönlich bevorzuge ich es, die Schnittstelle und die Klasse in derselben Datei zu haben, wenn es nur eine Klasse gibt, die die Schnittstelle implementiert, wie in Ihrem Fall.

In Bezug auf die Probleme, die Sie mit der Navigation haben, kann ich ReSharper wärmstens empfehlen . Es enthält einige sehr nützliche Verknüpfungen, mit denen Sie direkt zu der Methode springen können, die eine bestimmte Schnittstellenmethode implementiert.

Pete
quelle
3
+1 für den Hinweis, dass die Vorgehensweise des Teams die Dateistruktur vorschreiben sollte und dass der Ansatz der Norm zuwiderläuft.
3

In vielen Fällen möchten Sie, dass sich Ihre Schnittstelle nicht nur in einer separaten Datei für die Klasse befindet, sondern sogar in einer separaten Assembly insgesamt.

Zum Beispiel kann ein WCF - Service - Vertrag Schnittstelle werden geteilt durch Client und Service , wenn Sie die Kontrolle über beide Enden des Drahtes haben. Durch Verschieben der Schnittstelle in eine eigene Assembly werden weniger eigene Assembly-Abhängigkeiten erzeugt. Dies erleichtert dem Client den Verbrauch erheblich und lockert die Kopplung mit ihrer Implementierung.

StuartLC
quelle
2

Es ist selten, wenn überhaupt, sinnvoll, eine einzige Implementierung einer Schnittstelle zu haben 1 . Wenn Sie eine öffentliche Schnittstelle und eine öffentliche Klasse, die diese Schnittstelle implementiert, in dieselbe Datei einfügen, ist es gut möglich, dass Sie keine Schnittstelle benötigen.

Wenn die Klasse, die Sie gemeinsam mit der Schnittstelle suchen , abstrakt ist und Sie wissen, dass alle Implementierungen Ihrer Schnittstelle diese abstrakte Klasse erben sollten, ist es sinnvoll, die beiden in derselben Datei zu suchen. Sie sollten Ihre Entscheidung, eine Schnittstelle zu verwenden, dennoch hinterfragen: Fragen Sie sich, ob eine abstrakte Klasse für sich allein in Ordnung wäre, und lassen Sie die Schnittstelle fallen, wenn die Antwort positiv ist.

Im Allgemeinen sollten Sie sich jedoch an die Strategie "Eine öffentliche Klasse / Schnittstelle entspricht einer Datei" halten: Sie ist einfach zu befolgen und erleichtert die Navigation in Ihrem Quelltext.


1 Eine bemerkenswerte Ausnahme ist, wenn Sie zu Testzwecken eine Schnittstelle benötigen, da Ihre Wahl des Spott-Frameworks diese zusätzliche Anforderung an Ihren Code gestellt hat.

dasblinkenlight
quelle
3
Es ist in der Tat ein ganz normales Muster, eine Schnittstelle für eine Klasse in .NET zu haben, da Unit-Tests Abhängigkeiten durch Mocks, Stubs, Spys oder andere Test-Doubles ersetzen können.
Pete
2
@Pete Ich habe meine Antwort so bearbeitet, dass sie als Notiz angezeigt wird. Ich würde dieses Muster nicht als "normal" bezeichnen, da es Ihre Testbarkeitsbedenken "auslaufen" lässt. Moderne Mocking-Frameworks helfen Ihnen, dieses Problem weitgehend zu überwinden, aber eine Schnittstelle dort zu haben, vereinfacht die Dinge auf jeden Fall sehr.
dasblinkenlight
2
@KeithS Eine Klasse ohne Schnittstelle sollte nur dann in Ihrem Entwurf enthalten sein, wenn Sie absolut sicher sind, dass eine Unterklasse keinen Sinn ergibt, und wenn Sie diese Klasse erstellen sealed. Wenn Sie den Verdacht haben, dass Sie zu einem späteren Zeitpunkt eine zweite Unterklasse hinzufügen, fügen Sie sofort ein Interface hinzu. PS Ein anständiger Refactoring-Assistent kann Ihnen zu einem bescheidenen Preis für eine einzelne ReSharper-Lizenz gehören :)
dasblinkenlight
1
@KeithS Und ja, alle Ihre Klassen, die nicht speziell für die Unterklasse entwickelt wurden, sollten versiegelt werden. Tatsächlich wurden Wunschklassen standardmäßig versiegelt, wodurch Sie gezwungen wurden, explizit Klassen für die Vererbung festzulegen, so wie die Sprache Sie derzeit dazu zwingt, Funktionen zum Überschreiben festzulegen, indem Sie sie markieren virtual.
dasblinkenlight
2
@ KeithS: Was passiert, wenn aus einer Implementierung zwei werden? Dasselbe, wenn sich Ihre Implementierung ändert. Sie ändern den Code, um diese Änderung widerzuspiegeln. YAGNI bewirbt sich hier. Sie werden nie wissen, was sich in Zukunft ändern wird, also machen Sie das Einfachste, was möglich ist. (Leider testet Messes mit dieser Maxime)
Groky
2

Schnittstellen gehören zu ihren Kunden, nicht zu ihren Implementierungen, wie in Agile Principles, Practices und Patterns von Robert C. Martin definiert. Daher verstößt die Kombination von Schnittstelle und Implementierung an derselben Stelle gegen das Prinzip.

Der Client-Code hängt von der Schnittstelle ab. Dies ermöglicht das Kompilieren und Bereitstellen des Client-Codes und der Schnittstelle ohne Implementierung. Dann können Sie verschiedene Implementierungen haben, die wie Plugins für den Client-Code funktionieren .

UPDATE: Dies ist hier kein Agile-Only-Prinzip. Die vierköpfige Bande in Design Patterns aus dem Jahr 1994 spricht bereits von Kunden, die sich an Schnittstellen halten und an Schnittstellen programmieren. Ihre Ansicht ist ähnlich.

Patkos Csaba
quelle
1
Die Verwendung von Schnittstellen geht weit über die Domäne hinaus, die Agile besitzt. Agile ist eine Entwicklungsmethode, die auf besondere Weise Sprachkonstrukte verwendet. Ein Interface ist ein Sprachkonstrukt.
1
Ja, aber die Schnittstelle gehört immer noch dem Client und nicht der Implementierung, unabhängig davon, ob es sich um eine agile handelt oder nicht.
Patkos Csaba
Ich bin mit dir dabei.
Marjan Venema
0

Ich persönlich mag das "Ich" vor einer Schnittstelle nicht. Als Benutzer eines solchen Typs möchte ich nicht sehen, ob dies eine Schnittstelle ist oder nicht. Der Typ ist das Interessante. In Bezug auf Abhängigkeiten ist Ihr FooService EINE mögliche Implementierung von IFooService. Die Schnittstelle sollte sich in der Nähe des Ortes befinden, an dem sie verwendet wird. Andererseits sollte die Implementierung an einem Ort sein, an dem sie leicht geändert werden kann, ohne den Client zu beeinträchtigen. Daher würde ich zwei Dateien bevorzugen - normalerweise.

ollins
quelle
2
Als Benutzer möchte ich wissen, ob es sich bei einem Typ um eine Schnittstelle handelt, da dies beispielsweise bedeutet, dass ich ihn nicht verwenden kann new. Andererseits kann ich ihn auch als Co- oder Contra-Variante verwenden. Und das Iist eine in .Net etablierte Namenskonvention, daher ist es ein guter Grund, sich daran zu halten, wenn auch nur aus Gründen der Konsistenz mit anderen Typen.
Svick
Mit Iin interface zu beginnen war nur die Einführung in meine Argumente zur Trennung in zwei Dateien. Der Code ist besser lesbar und macht die Inversion der Abhängigkeiten deutlicher.
ollins
4
Ob es Ihnen gefällt oder nicht, die Verwendung eines 'I' vor einem Schnittstellennamen ist ein De-facto-Standard in .NET. Die Nichteinhaltung des Standards in einem .NET-Projekt wäre aus meiner Sicht eine Verletzung des Grundsatzes des geringsten Erstaunens.
Pete
Mein Hauptpunkt ist: in zwei Dateien trennen. Eine für die Abstraktion und eine für die Implementierung. Wenn die Verwendung von Iin Ihrer Umgebung normal ist: Verwenden Sie es! (Aber ich mag es nicht :))
ollins
Und im Fall von P.SE macht das Präfix I vor einer Schnittstelle deutlicher, dass es sich bei dem Poster um eine Schnittstelle handelt, die nicht von einer anderen Klasse erbt.
0

Die Kodierung von Schnittstellen kann schlecht sein, wird in der Tat als Anti-Pattern für bestimmte Kontexte behandelt und ist kein Gesetz. In der Regel ist ein gutes Beispiel für die Codierung in Anti-Pattern-Interfaces, wenn Sie ein Interface haben, das nur eine Implementierung in Ihrer Anwendung hat. Eine andere wäre, wenn die Schnittstellendatei so lästig wird, dass Sie ihre Deklaration in der Datei der implementierten Klasse verstecken müssen.

Shivan Drache
quelle
Nur eine Implementierung in Ihrer Anwendung einer Schnittstelle zu haben ist nicht unbedingt eine schlechte Sache. Man könnte die Anwendung vor technologischen Veränderungen schützen. Zum Beispiel eine Schnittstelle für eine Datenpersistenztechnologie. Sie hätten nur eine Implementierung für die aktuelle Datenpersistenz-Technologie, während Sie die App schützen würden, wenn sich dies ändern würde. Zu diesem Zeitpunkt würde es also nur einen Kobold geben.
TSmith
0

Indem Sie eine Klasse und eine Schnittstelle in dieselbe Datei einfügen, können Sie beide Funktionen ohne Einschränkungen verwenden. Eine Schnittstelle kann weiterhin auf die gleiche Weise für Mocks usw. verwendet werden, auch wenn sie sich in derselben Datei befindet wie eine Klasse, die sie implementiert. Die Frage könnte nur als organisatorische Bequemlichkeit aufgefasst werden. In diesem Fall würde ich sagen, tun Sie, was auch immer Ihnen das Leben erleichtert!

Natürlich könnte man argumentieren, dass die beiden in derselben Datei die Unachtsamkeit hervorrufen könnten, sie als programmgesteuerter zu betrachten, als sie es tatsächlich sind, was ein Risiko darstellt.

Wenn ich persönlich auf eine Codebasis stoßen würde, die eine Konvention über die andere verwendet, wäre ich in keiner Weise zu besorgt.

Holf
quelle
Es ist nicht nur eine "organisatorische Bequemlichkeit" ... es ist auch eine Frage des Risikomanagements, der Bereitstellungsverwaltung, der Code-Rückverfolgbarkeit, der Änderungskontrolle, der Lärmbelastung der Quellcodeverwaltung und der Sicherheitsverwaltung. Diese Frage hat viele Facetten.
TSmith