Was ist der Zweck einer Marker-Schnittstelle?

Antworten:

76

Dies ist eine Art Tangente, die auf der Antwort von "Mitch Wheat" basiert.

Wenn ich sehe, dass Leute die Richtlinien für das Framework-Design zitieren, möchte ich im Allgemeinen immer Folgendes erwähnen:

Im Allgemeinen sollten Sie die Richtlinien für das Framework-Design die meiste Zeit ignorieren.

Dies liegt nicht an Problemen mit den Richtlinien für das Framework-Design. Ich denke, das .NET Framework ist eine fantastische Klassenbibliothek. Ein Großteil dieser Fantastizität ergibt sich aus den Richtlinien für das Framework-Design.

Die Entwurfsrichtlinien gelten jedoch nicht für den meisten Code, der von den meisten Programmierern geschrieben wurde. Ihr Zweck ist es, die Erstellung eines großen Frameworks zu ermöglichen, das von Millionen von Entwicklern verwendet wird, um das Schreiben von Bibliotheken nicht effizienter zu gestalten.

Viele der darin enthaltenen Vorschläge können Sie dazu führen, Dinge zu tun, die:

  1. Möglicherweise nicht die einfachste Art, etwas zu implementieren
  2. Kann zu zusätzlicher Codeduplizierung führen
  3. Kann zusätzlichen Laufzeitaufwand haben

Das .net-Framework ist groß, wirklich groß. Es ist so groß, dass es absolut unvernünftig wäre anzunehmen, dass jemand detailliertes Wissen über jeden Aspekt davon hat. In der Tat ist es viel sicherer anzunehmen, dass die meisten Programmierer häufig auf Teile des Frameworks stoßen, die sie noch nie zuvor verwendet haben.

In diesem Fall sind die Hauptziele eines API-Designers:

  1. Halten Sie die Dinge mit dem Rest des Frameworks in Einklang
  2. Beseitigen Sie unnötige Komplexität in der API-Oberfläche

Die Framework-Design-Richtlinien fordern Entwickler dazu auf, Code zu erstellen, der diese Ziele erreicht.

Das bedeutet, Dinge wie das Vermeiden von Vererbungsebenen zu tun, selbst wenn dies das Duplizieren von Code bedeutet, oder den gesamten Code, der Ausnahmen auslöst, an "Einstiegspunkte" zu verschieben, anstatt gemeinsam genutzte Helfer zu verwenden (damit Stapelspuren im Debugger sinnvoller sind) und vieles mehr von anderen ähnlichen Dingen.

Der Hauptgrund, warum diese Richtlinien die Verwendung von Attributen anstelle von Markierungsschnittstellen vorschlagen, liegt darin, dass das Entfernen der Markierungsschnittstellen die Vererbungsstruktur der Klassenbibliothek wesentlich zugänglicher macht. Ein Klassendiagramm mit 30 Typen und 6 Ebenen der Vererbungshierarchie ist im Vergleich zu einem mit 15 Typen und 2 Hierarchieebenen sehr entmutigend.

Wenn wirklich Millionen von Entwicklern Ihre APIs verwenden oder Ihre Codebasis wirklich groß ist (z. B. über 100.000 LOC), kann das Befolgen dieser Richtlinien sehr hilfreich sein.

Wenn 5 Millionen Entwickler 15 Minuten damit verbringen, eine API zu lernen, anstatt 60 Minuten damit, sie zu lernen, ergibt sich eine Nettoeinsparung von 428 Mannjahren. Das ist viel Zeit.

An den meisten Projekten sind jedoch nicht Millionen von Entwicklern oder 100K + LOC beteiligt. In einem typischen Projekt mit beispielsweise 4 Entwicklern und etwa 50.000 Loc sind die Annahmen sehr unterschiedlich. Die Entwickler im Team haben ein viel besseres Verständnis für die Funktionsweise des Codes. Das bedeutet, dass es viel sinnvoller ist, zu optimieren, um schnell qualitativ hochwertigen Code zu erstellen und die Anzahl der Fehler sowie den Aufwand für Änderungen zu verringern.

Wenn Sie 1 Woche lang Code entwickeln, der mit dem .net-Framework kompatibel ist, und 8 Stunden lang Code schreiben, der einfach zu ändern ist und weniger Fehler aufweist, kann dies zu Folgendem führen:

  1. Späte Projekte
  2. Niedrigere Boni
  3. Erhöhte Fehleranzahl
  4. Mehr Zeit im Büro und weniger Zeit am Strand, um Margaritas zu trinken.

Ohne 4.999.999 andere Entwickler, die die Kosten übernehmen, lohnt es sich normalerweise nicht.

Das Testen auf Markierungsschnittstellen führt beispielsweise zu einem einzelnen "Ist" -Ausdruck und führt zu weniger Code als beim Suchen nach Attributen.

Mein Rat ist also:

  1. Befolgen Sie die Framework-Richtlinien religiös, wenn Sie Klassenbibliotheken (oder UI-Widgets) entwickeln, die für einen weit verbreiteten Verbrauch bestimmt sind.
  2. Erwägen Sie, einige davon zu übernehmen, wenn Sie mehr als 100.000 LOC in Ihrem Projekt haben
  3. Ansonsten ignoriere sie komplett.
Scott Wisniewski
quelle
12
Ich persönlich sehe jeden Code, den ich schreibe, als Bibliothek, die ich später verwenden muss. Ich kümmere mich nicht wirklich darum, ob der Verbrauch weit verbreitet ist oder nicht - das Befolgen von Richtlinien erhöht die Konsistenz und verringert die Überraschung, wenn ich meinen Code Jahre später ansehen und verstehen muss ...
Reed Copsey
16
Ich sage nicht, dass Richtlinien schlecht sind. Ich sage, sie sollten unterschiedlich sein, abhängig von der Größe Ihrer Codebasis und der Anzahl der Benutzer, die Sie haben. Viele der Designrichtlinien basieren auf Dingen wie der Aufrechterhaltung der binären Vergleichbarkeit, die für "interne" Bibliotheken, die von einer Handvoll Projekten verwendet werden, nicht so wichtig sind wie für etwas wie die BCL. Andere Richtlinien, wie die zur Benutzerfreundlichkeit, sind fast immer wichtig. Die Moral ist, nicht übermäßig religiös in Bezug auf die Richtlinien zu sein, insbesondere bei kleinen Projekten.
Scott Wisniewski
6
+1 - Hat die Frage des OP nicht ganz beantwortet - Zweck des MI - Aber trotzdem sehr hilfreich.
Bzarah
5
@ ScottWisniewski: Ich denke, Sie vermissen ernsthafte Punkte. Die Framework-Richtlinien gelten nur nicht für große Projekte, sondern für mittlere und einige kleine Projekte. Sie werden übertrieben, wenn Sie immer versuchen, sie auf das Hello-World-Programm anzuwenden. Beispielsweise ist die Beschränkung der Schnittstellen auf 5 Methoden unabhängig von der App-Größe immer eine gute Faustregel. Eine andere Sache, die Sie vermissen, die kleine App von heute kann die große App von morgen werden. Es ist also besser, wenn Sie es mit guten Prinzipien erstellen, die für große Apps gelten, damit Sie beim Skalieren nicht viel Code neu schreiben müssen.
Phil
2
Ich sehe nicht ganz ein, wie das Befolgen (der meisten) der Designrichtlinien zu einem 8-stündigen Projekt führen würde, das plötzlich 1 Woche dauert. Beispiel: Das Benennen einer virtual protectedVorlagenmethode DoSomethingCoreanstelle von DoSomethingist nicht so viel zusätzliche Arbeit und Sie kommunizieren klar, dass es sich um eine Vorlagenmethode handelt ... IMNSHO, die Leute, die Anwendungen schreiben, ohne die API ( But.. I'm not a framework developer, I don't care about my API!) zu berücksichtigen, sind genau die Leute, die viele Duplikate schreiben ( und auch undokumentierten und normalerweise unlesbaren Code, nicht umgekehrt.
Laoujin
44

Markierungsschnittstellen werden verwendet, um die Fähigkeit einer Klasse zu markieren, zur Laufzeit eine bestimmte Schnittstelle zu implementieren.

Die Richtlinien für das Interface-Design und das .NET-Typ-Design - Interface-Design raten von der Verwendung von Marker-Interfaces zugunsten der Verwendung von Attributen in C # ab. Wie @Jay Bazuzi jedoch betont, ist es einfacher, nach Marker-Interfaces zu suchen als nach Attributen:o is I

Also stattdessen:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

In den .NET-Richtlinien wurde Folgendes empfohlen:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 
Mitch Wheat
quelle
26
Wir können Generika auch vollständig mit Markierungsschnittstellen verwenden, jedoch nicht mit Attributen.
Jordão
18
Während ich Attribute liebe und wie sie von einem deklarativen Standpunkt aus aussehen, sind sie zur Laufzeit keine erstklassigen Bürger und erfordern eine erhebliche Menge an relativ einfachen Sanitärinstallationen, um damit zu arbeiten.
Jesse C. Slicer
4
@ Jordão - Das war genau mein Gedanke. Wenn ich beispielsweise den Datenbankzugriffscode (z. B. Linq zu SQL) abstrahieren möchte, macht es eine gemeinsame Schnittstelle VIEL einfacher. Tatsächlich glaube ich nicht, dass es möglich wäre, diese Art von Abstraktion mit Attributen zu schreiben, da Sie nicht in ein Attribut umwandeln und diese nicht in Generika verwenden können. Ich nehme an, Sie könnten eine leere Basisklasse verwenden, von der alle anderen Klassen abgeleitet sind, aber das fühlt sich mehr oder weniger so an, als hätte man eine leere Schnittstelle. Wenn Sie später feststellen, dass Sie gemeinsame Funktionen benötigen, ist der Mechanismus bereits vorhanden.
Tandrewnichols
23

Da in jeder anderen Antwort angegeben wurde, dass sie vermieden werden sollten, wäre es nützlich, eine Erklärung zu haben, warum.

Erstens, warum Markierungsschnittstellen verwendet werden: Sie sind vorhanden, damit der Code, der das Objekt verwendet, das sie implementiert, prüfen kann, ob sie diese Schnittstelle implementieren, und das Objekt in diesem Fall anders behandelt.

Das Problem bei diesem Ansatz ist, dass die Kapselung unterbrochen wird. Das Objekt selbst hat jetzt indirekte Kontrolle darüber, wie es extern verwendet wird. Darüber hinaus verfügt es über Kenntnisse über das System, in dem es verwendet werden soll. Durch Anwenden der Markierungsschnittstelle schlägt die Klassendefinition vor, dass es an einem Ort verwendet werden soll, der die Existenz des Markers überprüft. Es hat implizites Wissen über die Umgebung, in der es verwendet wird, und versucht zu definieren, wie es verwendet werden soll. Dies widerspricht der Idee der Kapselung, da es Kenntnisse über die Implementierung eines Teils des Systems hat, der vollständig außerhalb seines eigenen Bereichs existiert.

Auf praktischer Ebene verringert dies die Portabilität und Wiederverwendbarkeit. Wenn die Klasse in einer anderen Anwendung wiederverwendet wird, muss auch die Schnittstelle kopiert werden. In der neuen Umgebung hat sie möglicherweise keine Bedeutung, sodass sie vollständig redundant ist.

Als solches ist der "Marker" Metadaten über die Klasse. Diese Metadaten werden nicht von der Klasse selbst verwendet und sind nur für (einige!) Externen Client-Codes von Bedeutung, damit das Objekt auf eine bestimmte Weise behandelt werden kann. Da dies nur für den Clientcode von Bedeutung ist, sollten sich die Metadaten im Clientcode und nicht in der Klassen-API befinden.

Der Unterschied zwischen einer "Marker-Schnittstelle" und einer normalen Schnittstelle besteht darin, dass eine Schnittstelle mit Methoden der Außenwelt mitteilt, wie sie verwendet werden kann, während eine leere Schnittstelle impliziert, dass sie der Außenwelt mitteilt, wie sie verwendet werden soll.

Tom B.
quelle
1
Der Hauptzweck einer Schnittstelle besteht darin, zwischen Klassen zu unterscheiden, die versprechen, den mit dieser Schnittstelle verbundenen Vertrag einzuhalten, und solchen, die dies nicht tun. Während eine Schnittstelle auch für die Bereitstellung der aufrufenden Signaturen aller Mitglieder verantwortlich ist, die zur Erfüllung des Vertrags erforderlich sind, bestimmt der Vertrag und nicht die Mitglieder, ob eine bestimmte Schnittstelle von einer bestimmten Klasse implementiert werden soll. Wenn der Vertrag für IConstructableFromString<T>angibt, dass eine Klasse Tnur implementiert IConstructableFromString<T>werden darf, wenn sie ein statisches Mitglied hat ...
Supercat
... public static T ProduceFromString(String params);kann eine Begleitklasse zur Schnittstelle eine Methode anbieten public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; Wenn der Clientcode eine Methode wie diese hätte T[] MakeManyThings<T>() where T:IConstructableFromString<T>, könnte man neue Typen definieren, die mit dem Clientcode arbeiten könnten, ohne den Clientcode ändern zu müssen, um mit ihnen umzugehen. Wenn die Metadaten im Clientcode enthalten wären, könnten keine neuen Typen zur Verwendung durch den vorhandenen Client erstellt werden.
Supercat
Im Vertrag zwischen Tund der Klasse, die ihn verwendet, befindet sich IConstructableFromString<T>jedoch eine Methode in der Schnittstelle, die ein bestimmtes Verhalten beschreibt, sodass es sich nicht um eine Markierungsschnittstelle handelt.
Tom B
Die statische Methode, über die die Klasse verfügen muss, ist nicht Teil der Schnittstelle. Statische Elemente in Schnittstellen werden von den Schnittstellen selbst implementiert. Es gibt keine Möglichkeit für eine Schnittstelle, auf ein statisches Mitglied in einer implementierenden Klasse zu verweisen.
Superkatze
Es ist möglich, dass eine Methode mithilfe von Reflection ermittelt, ob ein generischer Typ zufällig über eine bestimmte statische Methode verfügt, und diese Methode ausführt, falls vorhanden. Der eigentliche Prozess zum Suchen und Ausführen der statischen Methode ProduceFromStringim obigen Beispiel würde dies jedoch nicht umfassen die Schnittstelle in irgendeiner Weise, außer dass die Schnittstelle als Markierung verwendet wird, um anzugeben, von welchen Klassen erwartet werden soll, dass sie die erforderliche Funktion implementieren.
Superkatze
8

Markierungsschnittstellen können manchmal ein notwendiges Übel sein, wenn eine Sprache keine diskriminierten Vereinigungstypen unterstützt .

Angenommen, Sie möchten eine Methode definieren, die ein Argument erwartet, dessen Typ genau A, B oder C sein muss. In vielen funktionalen Erstsprachen (wie F # ) kann ein solcher Typ sauber definiert werden als:

type Arg = 
    | AArg of A 
    | BArg of B 
    | CArg of C

In OO-Erstsprachen wie C # ist dies jedoch nicht möglich. Die einzige Möglichkeit, hier etwas Ähnliches zu erreichen, besteht darin, die Schnittstelle IArg zu definieren und damit A, B und C zu "markieren".

Natürlich könnten Sie die Verwendung der Markierungsschnittstelle vermeiden, indem Sie einfach den Typ "Objekt" als Argument akzeptieren, aber dann würden Sie die Ausdruckskraft und ein gewisses Maß an Typensicherheit verlieren.

Diskriminierte Gewerkschaftstypen sind äußerst nützlich und existieren seit mindestens 30 Jahren in funktionalen Sprachen. Seltsamerweise haben bis heute alle gängigen OO-Sprachen diese Funktion ignoriert - obwohl sie eigentlich nichts mit funktionaler Programmierung an sich zu tun hat, sondern zum Typensystem gehört.

Marc Sigrist
quelle
Es ist erwähnenswert, dass es nicht schwierig ist, eine generische Klasse statische Felder mit Delegaten für die Verarbeitung von a zu haben und diese Felder mit Funktionen vorzufüllen, die für jeden Typ der Klasse geeignet sind, da a Foo<T>für jeden Typ einen eigenen Satz Tstatischer Felder enthält Tsoll mit arbeiten. Die Verwendung einer generischen Schnittstellenbeschränkung für den Typ Twürde zum Zeitpunkt des Compilers prüfen, ob der angegebene Typ zumindest die Gültigkeit beansprucht, obwohl er nicht sicherstellen kann, dass er tatsächlich gültig ist.
Supercat
6

Eine Markierungsschnittstelle ist nur eine leere Schnittstelle. Eine Klasse würde diese Schnittstelle als Metadaten implementieren, die aus irgendeinem Grund verwendet werden sollen. In C # verwenden Sie häufiger Attribute, um eine Klasse zu markieren, aus den gleichen Gründen, aus denen Sie eine Markierungsschnittstelle in anderen Sprachen verwenden würden.

Richard Anthony Hein
quelle
4

Über eine Markierungsschnittstelle kann eine Klasse so markiert werden, dass sie auf alle untergeordneten Klassen angewendet wird. Eine "reine" Marker-Schnittstelle würde nichts definieren oder erben. Ein nützlicherer Typ von Markierungsschnittstellen kann eine sein, die eine andere Schnittstelle "erbt", aber keine neuen Mitglieder definiert. Wenn es beispielsweise eine Schnittstelle "IReadableFoo" gibt, könnte man auch eine Schnittstelle "IImmutableFoo" definieren, die sich wie ein "Foo" verhält, aber jedem, der sie verwendet, verspricht, dass nichts ihren Wert ändert. Eine Routine, die ein IImmutableFoo akzeptiert, kann es wie ein IReadableFoo verwenden, aber die Routine akzeptiert nur Klassen, die als Implementierung von IImmutableFoo deklariert wurden.

Ich kann mir nicht viele Verwendungsmöglichkeiten für "reine" Marker-Interfaces vorstellen. Das einzige, an das ich denken kann, wäre, wenn EqualityComparer (von T) .Default Object.Equals für jeden Typ zurückgeben würde, der IDoNotUseEqualityComparer implementiert, selbst wenn der Typ auch IEqualityComparer implementiert. Dies würde es einem ermöglichen, einen nicht versiegelten unveränderlichen Typ zu haben, ohne das Liskov-Substitutionsprinzip zu verletzen: Wenn der Typ alle Methoden im Zusammenhang mit Gleichheitsprüfungen versiegelt, könnte ein abgeleiteter Typ zusätzliche Felder hinzufügen und sie veränderbar machen, aber die Mutation solcher Felder würde dies nicht tun. ' Sie können mit Basismethoden nicht sichtbar sein. Es ist möglicherweise nicht schrecklich, eine nicht versiegelte unveränderliche Klasse zu haben und entweder die Verwendung von EqualityComparer.Default zu vermeiden oder abgeleitete Klassen zu vertrauen, um IEqualityComparer nicht zu implementieren.

Superkatze
quelle
4

Diese beiden Erweiterungsmethoden lösen die meisten Probleme, von denen Scott behauptet, dass sie Markierungsschnittstellen gegenüber Attributen bevorzugen:

public static bool HasAttribute<T>(this ICustomAttributeProvider self)
    where T : Attribute
{
    return self.GetCustomAttributes(true).Any(o => o is T);
}

public static bool HasAttribute<T>(this object self)
    where T : Attribute
{
    return self != null && self.GetType().HasAttribute<T>()
}

Jetzt hast du:

if (o.HasAttribute<FooAssignableAttribute>())
{
    //...
}

gegen:

if (o is IFooAssignable)
{
    //...
}

Ich kann nicht sehen, wie das Erstellen einer API mit dem ersten Muster fünfmal so lange dauert wie mit dem zweiten, wie Scott behauptet.

Herr Anderson
quelle
1
Immer noch keine Generika.
Ian Kemp
1

Die Marker-Schnittstelle ist eigentlich nur eine prozedurale Programmierung in einer OO-Sprache. Eine Schnittstelle definiert einen Vertrag zwischen Implementierern und Verbrauchern mit Ausnahme einer Markierungsschnittstelle, da eine Markierungsschnittstelle nur sich selbst definiert. Die Markierungsschnittstelle versagt also direkt vor dem Tor, um eine Schnittstelle zu sein.

Ali Bayat
quelle
0

Marker sind leere Schnittstellen. Ein Marker ist entweder da oder nicht.

Klasse Foo: IConfidential

Hier markieren wir Foo als vertraulich. Keine tatsächlichen zusätzlichen Eigenschaften oder Attribute erforderlich.

Rick O'Shea
quelle
0

Die Marker-Schnittstelle ist eine vollständig leere Schnittstelle ohne Body / Datenelemente / Implementierung.
Eine Klasse implementiert bei Bedarf eine Markierungsschnittstelle. Sie dient lediglich zum " Markieren ". bedeutet, dass die JVM darüber informiert wird, dass die bestimmte Klasse zum Klonen bestimmt ist. Lassen Sie sie daher klonen. Diese spezielle Klasse dient zum Serialisieren ihrer Objekte. Lassen Sie daher zu, dass ihre Objekte serialisiert werden.

Arun Raaj
quelle