Ist es sinnvoll, eine Schnittstelle zu definieren, wenn ich bereits eine abstrakte Klasse habe?

12

Ich habe eine Klasse mit einigen Standard- / gemeinsamen Funktionen. Ich benutze abstract classdafür:

public interface ITypeNameMapper
{
    string Map(TypeDefinition typeDefinition);
}

public abstract class TypeNameMapper : ITypeNameMapper
{
    public virtual string Map(TypeDefinition typeDefinition)
    {
        if (typeDefinition is ClassDefinition classDefinition)
        {
            return Map(classDefinition);
        }
        ...

        throw new ArgumentOutOfRangeException(nameof(typeDefinition));
    }

    protected abstract string Map(ClassDefinition classDefinition);
}

Wie Sie sehen, habe ich auch die Schnittstelle ITypeNameMapper. Ist es sinnvoll, diese Schnittstelle zu definieren, wenn ich bereits eine abstrakte Klasse habe TypeNameMapperoder abstract classgerade genug?

TypeDefinition in diesem minimalen Beispiel ist auch abstrakt.

Konrad
quelle
4
Nur zu Ihrer Information: In OOD bedeutet das Überprüfen von Typen im Code, dass Ihre Klassenhierarchie nicht korrekt ist. Sie müssen abgeleitete Klassen aus dem Typ erstellen, den Sie überprüfen. In diesem Fall möchten Sie eine Schnittstelle ITypeMapper, die eine Map () - Methode angibt, und diese Schnittstelle dann sowohl in der TypeDefinition-Klasse als auch in der ClassDefinition-Klasse implementieren. Auf diese Weise durchläuft Ihr ClassAssembler einfach die Liste der ITypeMappers und ruft für jeden Map () auf. Von beiden Typen wird die gewünschte Zeichenfolge abgerufen, da jeder Typ eine eigene Implementierung hat.
Rodney P. Barbati
1
Die Verwendung einer Basisklasse anstelle einer Schnittstelle wird als "Brennen der Basis" bezeichnet, sofern dies nicht unbedingt erforderlich ist. Wenn dies mit einer Schnittstelle möglich ist, tun Sie dies mit einer Schnittstelle. Die abstrakte Klasse wird dann zu einer hilfreichen Ergänzung. artima.com/intv/dotnet.html Insbesondere erfordert , dass Sie Remoting von MarshalByRefObject so abzuleiten , wenn Sie Sie werden remoted wollen nicht , kann von allem anderen ableiten.
Ben
@ RodneyP.Barbati wird nicht funktionieren, wenn ich viele Mapper für den gleichen Typ haben möchte, den ich je nach Situation wechseln kann
Konrad
1
@Ben brennt die Basis, die interessant ist. Danke für das Juwel!
Konrad
@Konrad Das ist falsch - nichts hindert Sie daran, die Schnittstelle durch Aufrufen einer anderen Implementierung der Schnittstelle zu implementieren. Daher kann jeder Typ eine steckbare Implementierung haben, die Sie über die Implementierung im Typ verfügbar machen.
Rodney P. Barbati

Antworten:

31

Ja, da C # die Mehrfachvererbung nur mit Schnittstellen zulässt.

Wenn ich also eine Klasse habe, die sowohl TypeNameMapper als auch SomethingelseMapper ist, kann ich Folgendes tun:

class MultiFunctionalClass : ITypeNameMapper, ISomethingelseMapper 
{
    private TypeNameMapper map1
    private SomethingelseMapper map2

    public string Map(TypeDefinition typeDefinition) { return map1.Map(typeDefintion);}

    public string Map(OtherDef otherDef) { return map2.Map(orderDef); }
}
Ewan
quelle
4
@ Liath: Einzelverantwortung ist tatsächlich ein Faktor, der darüber entscheidet, warum Vererbung erforderlich ist. Betrachten wir drei mögliche Züge: ICanFly, ICanRun, ICanSwim. Sie müssen 8 Kreaturenklassen erstellen, eine für jede Kombination von Merkmalen (JJJ, JJJ, JJJ, ..., NJJ, NJJ). SRP gibt an, dass Sie jedes Verhalten (Fliegen, Laufen, Schwimmen) einmal implementieren und diese dann auf den 8 Kreaturen wiederverwendbar implementieren. Die Komposition wäre hier sogar noch besser, aber wenn Sie die Komposition für eine Sekunde ignorieren, sollten Sie bereits die Notwendigkeit erkennen, aufgrund von SRP mehrere separate wiederverwendbare Verhalten zu erben / implementieren .
Flater
4
@Konrad das meine ich. Wenn Sie die Schnittstelle schreiben und sie nie benötigen, betragen die Kosten 3 LoC. Wenn Sie die Schnittstelle nicht schreiben und feststellen, dass Sie sie benötigen, werden die Kosten möglicherweise für die Umgestaltung Ihrer gesamten Anwendung anfallen.
Ewan
3
(1) Schnittstellen implementieren ist Vererbung? (2) Die Frage und die Antwort sollten beide qualifiziert sein. "Es hängt davon ab, ob." (3) Mehrfachvererbung erfordert einen Warnaufkleber. (4) Ich könnte Ihnen K's von craptacular formal mispractice zeigen interface. Redundante Implementierungen, die in der Basis sein sollten - die sich unabhängig entwickeln! - Bedingte Logikänderungen, die diesen Müll aus Mangel an Template-Methoden, kaputter Kapselung und nicht existierender Abstraktion antreiben. Mein Favorit: Klassen mit einer Methode interface, von denen jede eine eigene Methode mit nur einer Instanz implementiert . ... usw. usw. und usw.
Radarbob
3
Es gibt einen unsichtbaren Reibungskoeffizienten im Code. Sie spüren es, wenn Sie gezwungen sind, überflüssige Schnittstellen zu lesen. Dann erwärmt es sich wie das Space Shuttle, das in die Atmosphäre knallt, als Interface-Implementierung der abstrakten Klasse. Es ist die Twilight Zone und das ist der Shuttle Challenger. Ich akzeptiere mein Schicksal und beobachte das rasende Plasma mit furchtlosem, freistehendem Staunen. Unbarmherzige Reibung zerreißt die Fassade der Perfektion und bringt den zerschmetterten Rumpf mit einem donnernden Knall in die Produktion.
Radarbob
4
@ Radarbob Poetic. ^ _ ^ Ich würde zustimmen; Die Kosten für Schnittstellen sind zwar gering, aber nicht existent. Nicht so sehr beim Schreiben, aber in einer Debugging-Situation sind es 1-2 weitere Schritte, um zum tatsächlichen Verhalten zu gelangen, was sich schnell summieren kann, wenn Sie ein halbes Dutzend Abstraktionsebenen tief erreichen. In einer Bibliothek lohnt es sich normalerweise. In einer Anwendung ist Refactoring jedoch besser als eine vorzeitige Verallgemeinerung.
Errorsatz
1

Schnittstellen und abstrakte Klassen dienen verschiedenen Zwecken:

  • Schnittstellen definieren APIs und gehören zu den Clients, nicht zu den Implementierungen.
  • Wenn Klassen Implementierungen gemeinsam nutzen, können Sie von einer abstrakten Klasse profitieren .

Definiert in Ihrem Beispiel interface ITypeNameMapperdie Bedürfnisse der Kunden und abstract class TypeNameMapperbietet keinen Mehrwert.

Geoffrey Hale
quelle
2
Auch abstrakte Klassen gehören zu den Kunden. Ich weiß nicht, was du damit meinst. Alles was publicdem Kunden gehört. TypeNameMappererhöht den Wert erheblich, da der Implementierer keine gemeinsame Logik schreiben muss.
Konrad
2
Ob eine Schnittstelle als dargestellt wird interfaceoder nicht, ist nur dann relevant, wenn C # eine vollständige Mehrfachvererbung verbietet.
Deduplizierer
0

Das gesamte Konzept der Schnittstellen wurde erstellt, um eine Klassenfamilie mit einer gemeinsam genutzten API zu unterstützen.

Dies bedeutet ausdrücklich, dass jede Verwendung einer Schnittstelle impliziert, dass mehr als eine Implementierung ihrer Spezifikation vorhanden ist (oder erwartet wird).

DI-Frameworks haben das Wasser hier getrübt, da viele von ihnen eine Schnittstelle benötigen, obwohl es immer nur eine Implementierung geben wird - für mich ist dies ein unangemessener Aufwand für das, was in den meisten Fällen nur ein komplexeres und langsameres Mittel zum Aufrufen von Neuem ist, aber es es ist was es ist.

Die Antwort liegt in der Frage selbst. Wenn Sie eine abstrakte Klasse haben, bereiten Sie die Erstellung mehrerer abgeleiteter Klassen mit einer gemeinsamen API vor. Somit ist die Verwendung einer Schnittstelle eindeutig gekennzeichnet.

Rodney P. Barbati
quelle
2
"Wenn Sie eine abstrakte Klasse haben, ... wird die Verwendung einer Schnittstelle deutlich angezeigt." - Mein Fazit ist das Gegenteil: Wenn Sie eine abstrakte Klasse haben, verwenden Sie diese als Schnittstelle (wenn die DI nicht damit spielt, ziehen Sie eine andere Implementierung in Betracht). Erstellen Sie das Interface nur, wenn es wirklich nicht funktioniert - Sie können es später jederzeit tun.
Maaartinus
1
Das Gleiche gilt für @maaartinus. Und nicht einmal "... als wäre es eine Schnittstelle." Die öffentlichen Mitglieder einer Klasse sind eine Schnittstelle. Das Gleiche gilt für "Sie können es später jederzeit tun". Dies geht auf das Herzstück der Designgrundlagen 101.
Radarbob
@maaartinus: Wenn man von Anfang an eine Schnittstelle erstellt, aber überall Referenzen vom Typ abstract class verwendet, hilft die Existenz der Schnittstelle nichts. Wenn der Client-Code jedoch nach Möglichkeit den Schnittstellentyp anstelle des Typs der abstrakten Klasse verwendet, erleichtert dies die Arbeit erheblich, wenn eine Implementierung erstellt werden muss, die sich intern stark von der abstrakten Klasse unterscheidet. Das Ändern des Client-Codes an diesem Punkt zur Verwendung der Benutzeroberfläche ist weitaus schmerzhafter als das Entwerfen von Client-Code, um dies von Anfang an zu tun.
Supercat
1
@supercat Ich könnte zustimmen, wenn Sie eine Bibliothek schreiben. Wenn es sich um eine Anwendung handelt, kann ich kein Problem feststellen, bei dem alle Vorkommen gleichzeitig ersetzt werden. Meine Eclipse kann dies (Java, nicht C #), aber wenn dies nicht möglich ist, werden find ... -exec perl -pi -e ...möglicherweise geringfügige Kompilierungsfehler behoben. Mein Punkt ist: Der Vorteil, kein (derzeit) nutzloses Ding zu haben, ist viel größer als die möglichen zukünftigen Einsparungen.
Maaartinus
Der erste Satz wäre korrekt, wenn Sie sich interfaceals Code auszeichnen (Sie sprechen von C # - interfaces, nicht von einer abstrakten Schnittstelle, wie jeder Menge von Klassen / Funktionen / etc) und ihn beenden würden "... aber immer noch ein vollständiges Vielfaches verbieten Erbe." Es gibt keine Notwendigkeit für interfaces, wenn Sie MI haben.
Deduplizierer
0

Wenn wir die Frage explizit beantworten möchten, sagt der Autor: "Ist es sinnvoll, diese Schnittstelle zu definieren, wenn ich bereits eine abstrakte Klasse TypeNameMapper habe oder eine abstrakte Klasse gerade ausreicht?"

Die Antwort lautet "Ja" und "Nein". Ja, Sie sollten die Schnittstelle erstellen, obwohl Sie bereits über die abstrakte Basisklasse verfügen (da Sie in keinem Clientcode auf die abstrakte Basisklasse verweisen sollten), und "Nein", weil Sie keine Basisklasse erstellen sollten die abstrakte Basisklasse, wenn keine Schnittstelle vorhanden ist.

Es ist offensichtlich, dass Sie nicht genügend Gedanken in die API gesteckt haben, die Sie erstellen möchten. Überlegen Sie sich zuerst die API und führen Sie dann bei Bedarf eine teilweise Implementierung durch. Die Tatsache, dass Ihre abstrakte Basisklasse Code eincheckt, reicht aus, um Ihnen mitzuteilen, dass es sich nicht um die richtige Abstraktion handelt.

Wie in den meisten OOD-Situationen informiert Sie Ihr Code darüber, was als Nächstes kommt, wenn Sie sich in der Rille befinden und Ihr Objektmodell gut ausgearbeitet haben. Beginnen Sie mit einer Schnittstelle und implementieren Sie diese Schnittstelle in den Klassen, die Sie benötigen. Wenn Sie feststellen, dass Sie ähnlichen Code schreiben, extrahieren Sie ihn in eine abstrakte Basisklasse - es ist die Schnittstelle, die wichtig ist, die abstrakte Basisklasse ist nur ein Helfer, und es kann mehr als einen geben.

Rodney P. Barbati
quelle
-1

Dies ist ziemlich schwierig zu beantworten, ohne den Rest der Anwendung zu kennen.

Angenommen, Sie verwenden ein DI-Modell und haben Ihren Code so erweiterbar wie möglich gestaltet (so dass Sie TypeNameMapperleicht neue hinzufügen können), würde ich Ja sagen.

Gründe für:

  • Sie erhalten es praktisch kostenlos, da die Basisklasse das implementiert interface, worüber Sie sich bei untergeordneten Implementierungen keine Gedanken machen müssen
  • Die meisten IoC-Frameworks und Mocking-Bibliotheken erwarten interfaces. Obwohl viele von ihnen mit abstrakten Klassen arbeiten, ist dies nicht immer selbstverständlich, und ich bin fest davon überzeugt, dass ich den gleichen Weg wie die anderen 99% der Bibliotheksbenutzer einschlagen werde, wenn dies möglich ist.

Gründe gegen:

  • Genau genommen (wie Sie bereits betont haben) müssen Sie das nicht WIRKLICH tun
  • Wenn sich das class/ interfaceändert, entsteht ein wenig zusätzlicher Aufwand

Alles in allem würde ich sagen, dass Sie das schaffen sollten interface. Die Gründe dafür sind vernachlässigbar, aber obwohl es möglich ist, Abstracts in Mocking und IoC zu verwenden, ist es mit Interfaces oft viel einfacher. Letztendlich würde ich jedoch den Code eines Kollegen nicht kritisieren, wenn sie den umgekehrten Weg gehen würden.

Liath
quelle