Wird eine Schnittstelle als "leer" betrachtet, wenn sie von anderen Schnittstellen erbt?

12

Leere Interfaces werden, soweit ich das beurteilen kann, im Allgemeinen als schlechte Praxis eingestuft - insbesondere, wenn Dinge wie Attribute von der Sprache unterstützt werden.

Wird eine Schnittstelle jedoch als "leer" betrachtet, wenn sie von anderen Schnittstellen erbt?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Alles , was die Geräte I3benötigen zu implementieren I1und I2, und Objekte aus verschiedenen Klassen , die erben I3kann dann untereinander ausgetauscht werden (siehe unten), so ist es richtig zu nennen I3 leer ? Wenn ja, was wäre ein besserer Weg, dies zu architektonisieren?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}
Kai
quelle
1
Die Unfähigkeit, mehrere Schnittstellen als Typ eines Parameters / Felds usw. anzugeben, ist ein Konstruktionsfehler in C # / .NET.
CodesInChaos
Ich denke, der bessere Weg zur Architektur dieses Teils sollte in einer separaten Frage behandelt werden. Im Moment hat die akzeptierte Antwort nichts mit dem Fragentitel zu tun.
Kapol
@CodesInChaos Nein, das ist nicht so, da es bereits zwei Möglichkeiten gibt, dies zu tun. Eine ist in dieser Frage, die andere wäre, einen generischen Typparameter für das Methodenargument anzugeben und die where-Einschränkung zu verwenden, um beide Schnittstellen anzugeben.
Andy
Ist es sinnvoll, dass eine Klasse, die I1 und I2 implementiert, aber nicht I3, nicht von verwendet werden kann foo? (Wenn die Antwort "Ja" lautet, fahren Sie fort!)
user253751
@Andy Obwohl ich der Behauptung von CodesInChaos, dass es sich um einen Fehler handelt , nicht ganz zustimme , halte ich generische Typparameter der Methode nicht für eine Lösung - es überträgt die Verantwortung, einen in der Implementierung der Methode verwendeten Typ zu kennen ruft die Methode auf, was sich falsch anfühlt. Vielleicht wäre eine Syntax wie var : I1, I2 something = new A();für lokale Variablen nett (wenn wir die guten Punkte, die in Konrads Antwort aufgeworfen wurden, ignorieren)
Kai

Antworten:

27

Alles, was I3 implementiert, muss I1 und I2 implementieren, und Objekte aus verschiedenen Klassen, die I3 erben, können dann austauschbar verwendet werden (siehe unten). Ist es also richtig, I3 leer aufzurufen? Wenn ja, was wäre ein besserer Weg, dies zu architektonisieren?

"Bundling" I1und " I2into" I3bieten eine nette (?) Verknüpfung oder eine Art Syntaxzucker für alle Anwendungen, für die Sie beide implementieren möchten.

Das Problem bei diesem Ansatz ist , dass Sie nicht andere Entwickler von explizit Implementierung stoppen können I1 und I2 nebeneinander, aber es funktioniert nicht in beide Richtungen - während : I3entspricht der : I1, I2, : I1, I2ist kein Äquivalent : I3. Auch wenn sie funktional identisch sind, eine Klasse, die beide unterstützt I1und I2nicht als unterstützend erkannt wird I3.

Dies bedeutet, dass Sie zwei Möglichkeiten anbieten, um dasselbe Ziel zu erreichen: "manuell" und "süß" (mit Ihrem Syntaxzucker). Dies kann sich negativ auf die Codekonsistenz auswirken. Das Anbieten von 2 verschiedenen Möglichkeiten, um das Gleiche hier, dort und an einer anderen Stelle zu erreichen, führt bereits zu 8 möglichen Kombinationen :)

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

OK, das kannst du nicht. Aber vielleicht ist es tatsächlich ein gutes Zeichen?

Wenn I1und I2implizieren unterschiedliche Verantwortlichkeiten (ansonsten wäre es gar nicht nötig, sie zu trennen), dann ist es vielleicht foo()falsch something, zwei unterschiedliche Verantwortlichkeiten auf einmal ausführen zu lassen?

Mit anderen Worten, es könnte sein, dass dies foo()selbst gegen das Prinzip der Einzelverantwortung verstößt, und die Tatsache, dass Casting und Laufzeit-Typprüfungen erforderlich sind, ist eine rote Fahne, die uns alarmiert.

BEARBEITEN:

Wenn Sie darauf bestehen möchten, foodass für die Implementierung von I1und Folgendes erforderlich ist I2, können Sie diese als separate Parameter übergeben:

void foo(I1 i1, I2 i2) 

Und sie könnten dasselbe Objekt sein:

foo(something, something);

Was fookümmert es, ob sie dieselbe Instanz sind oder nicht?

Aber wenn es Ihnen etwas ausmacht (zB weil sie nicht staatenlos sind), oder wenn Sie einfach denken, dass dies hässlich aussieht, könnten Sie stattdessen Generika verwenden:

void Foo<T>(T something) where T: I1, I2

Jetzt sorgen allgemeine Beschränkungen für den gewünschten Effekt, ohne die Außenwelt mit irgendetwas zu verschmutzen. Dies ist ein idiomatisches C #, das auf Prüfungen zur Kompilierungszeit basiert und sich insgesamt besser anfühlt.

Ich sehe I3 : I2, I1etwas als Versuch zu emulieren Union - Typ in einer Sprache , die sie nicht von der Box unterstützt aus (C # oder Java nicht, während Union - Typen in funktionalen Sprachen existieren).

Der Versuch, ein Konstrukt zu emulieren, das von der Sprache nicht wirklich unterstützt wird, ist oft umständlich. Ebenso können Sie in C # Erweiterungsmethoden und Schnittstellen verwenden, wenn Sie Mixins emulieren möchten . Möglich? Ja. Gute Idee? Ich bin mir nicht sicher.

Konrad Morawski
quelle
4

Wenn es besonders wichtig war, dass eine Klasse beide Schnittstellen implementiert, sehe ich nichts falsches daran, eine dritte Schnittstelle zu erstellen, die von beiden erbt. Ich würde jedoch darauf achten, nicht in die Falle der Überentwicklung zu geraten. Eine Klasse könnte von der einen oder der anderen oder von beiden erben. Wenn mein Programm in diesen drei Fällen nicht viele Klassen hatte, ist es ein wenig schwierig, eine Schnittstelle für beide zu erstellen, und schon gar nicht, wenn diese beiden Schnittstellen keine Beziehung zueinander hatten (bitte keine IComparableAndSerializable-Schnittstellen).

Ich erinnere Sie daran, dass Sie auch eine abstrakte Klasse erstellen können, die von beiden Schnittstellen erbt, und dass Sie diese abstrakte Klasse anstelle von I3 verwenden können. Ich denke, es wäre falsch zu sagen, dass Sie es nicht tun sollten, aber Sie sollten zumindest einen guten Grund haben.

Neil
quelle
Huh? Sie können durchaus zwei Interfaces in einer Klasse implementieren. Sie erben keine Schnittstellen, Sie implementieren sie.
Andy
@Andy Wo habe ich gesagt, dass eine einzelne Klasse nicht zwei Schnittstellen implementieren kann? Denn was die Verwendung des Wortes "erben" anstelle von "implementieren" betrifft, ist Semantik. In C ++ würde die Vererbung einwandfrei funktionieren, da abstrakte Klassen den Schnittstellen am nächsten kommen .
Neil
Vererbung und Schnittstellenimplementierung sind nicht dieselben Konzepte. Klassen, die mehrere Unterklassen erben, können zu seltsamen Problemen führen, was bei der Implementierung mehrerer Schnittstellen nicht der Fall ist. Und das Fehlen von Schnittstellen in C ++ bedeutet nicht, dass der Unterschied verschwindet, sondern dass C ++ in seinen Möglichkeiten eingeschränkt ist. Vererbung ist eine "Ist-Ist" -Beziehung, während Schnittstellen "Kann-Tun" angeben.
Andy
@ Andy Weird-Probleme, die durch Kollisionen zwischen in diesen Klassen implementierten Methoden verursacht werden, ja. Dies ist jedoch keine Schnittstelle mehr, weder im Namen noch in der Praxis. Abstrakte Klassen, die Methoden deklarieren, aber nicht implementieren, sind im wahrsten Sinne des Wortes mit Ausnahme von name eine Schnittstelle. Die Terminologie ändert sich zwischen den Sprachen, aber das bedeutet nicht, dass eine Referenz und ein Zeiger nicht dasselbe Konzept sind. Es ist ein umständlicher Punkt, aber wenn Sie darauf bestehen, müssen wir zustimmen, dass wir nicht zustimmen.
Neil
"Terminologie wechselt zwischen Sprachen" Nein, tut es nicht. Die OO-Terminologie ist sprachübergreifend konsistent. Ich habe noch nie eine abstrakte Klasse gehört, die in jeder von mir verwendeten OO-Sprache etwas anderes bedeutet. Ja, Sie können die Semantik einer Schnittstelle in C ++ haben, aber der Punkt ist, dass nichts jemanden daran hindert, einer abstrakten Klasse in dieser Sprache Verhalten hinzuzufügen und diese Semantik zu brechen.
Andy
2

Leere Schnittstellen gelten im Allgemeinen als schlechte Praxis

Ich stimme dir nicht zu. Lesen Sie mehr über Marker-Interfaces .

Während eine typische Schnittstelle Funktionen (in Form von Methodendeklarationen) angibt, die eine implementierende Klasse unterstützen muss, muss dies eine Markierungsschnittstelle nicht tun. Das bloße Vorhandensein einer solchen Schnittstelle weist auf ein bestimmtes Verhalten der implementierenden Klasse hin.

Radarbob
quelle
Ich stelle mir vor, das OP ist sich ihrer bewusst, aber sie sind eigentlich ein bisschen umstritten. Während einige Autoren sie empfehlen (z. B. Josh Bloch in "Effective Java"), behaupten einige, sie seien ein Gegenmuster und Erbe aus Zeiten, in denen Java noch keine Anmerkungen hatte. (Anmerkungen in Java entsprechen in etwa den Attributen in .NET.) Mein Eindruck ist, dass sie in der Java-Welt viel beliebter sind als in .NET.
Konrad Morawski
1
Ich benutze sie ab und zu, aber es fühlt sich irgendwie hackisch an - die Schattenseiten sind, dass man nicht anders kann, als dass sie geerbt wurden. Wenn man eine Klasse mit einer markiert, bekommen alle ihre Kinder markiert auch, ob Sie es wollen oder nicht. Und sie scheinen schlechte Praktiken zu fördern, instanceofanstatt Polymorphismus zu verwenden
Konrad Morawski