À quoi sert une interface de marqueurs?

Réponses:

77

C'est un peu une tangente basée sur la réponse de "Mitch Wheat".

En général, chaque fois que je vois des gens citer les directives de conception du cadre, j'aime toujours le mentionner:

Vous devez généralement ignorer les directives de conception du cadre la plupart du temps.

Ce n'est pas à cause d'un problème avec les directives de conception du cadre. Je pense que le framework .NET est une bibliothèque de classes fantastique. Une grande partie de cette fantaisie découle des directives de conception du cadre.

Cependant, les directives de conception ne s'appliquent pas à la plupart des codes écrits par la plupart des programmeurs. Leur objectif est de permettre la création d'un large framework utilisé par des millions de développeurs, et non de rendre l'écriture de bibliothèque plus efficace.

Un grand nombre de ses suggestions peuvent vous guider pour faire des choses qui:

  1. Ce n'est peut-être pas le moyen le plus simple de mettre en œuvre quelque chose
  2. Peut entraîner une duplication de code supplémentaire
  3. Peut avoir une surcharge d'exécution supplémentaire

Le framework .net est grand, vraiment grand. C'est tellement important qu'il serait absolument déraisonnable de supposer que quiconque a des connaissances détaillées sur tous ses aspects. En fait, il est beaucoup plus sûr de supposer que la plupart des programmeurs rencontrent fréquemment des parties du framework qu'ils n'ont jamais utilisées auparavant.

Dans ce cas, les principaux objectifs d'un concepteur d'API sont les suivants:

  1. Gardez les choses cohérentes avec le reste du cadre
  2. Éliminez la complexité inutile dans la zone de surface de l'API

Les directives de conception du cadre poussent les développeurs à créer du code qui atteint ces objectifs.

Cela signifie faire des choses comme éviter les couches d'héritage, même si cela signifie dupliquer du code, ou pousser tous les codes d'exception vers des «points d'entrée» plutôt que d'utiliser des helpers partagés (pour que les traces de pile aient plus de sens dans le débogueur), et beaucoup d'autres choses similaires.

La principale raison pour laquelle ces directives suggèrent d'utiliser des attributs au lieu d'interfaces de marqueurs est que la suppression des interfaces de marqueurs rend la structure d'héritage de la bibliothèque de classes beaucoup plus accessible. Un diagramme de classes avec 30 types et 6 couches de hiérarchie d'héritage est très intimidant par rapport à un avec 15 types et 2 couches de hiérarchie.

S'il y a vraiment des millions de développeurs qui utilisent vos API ou que votre base de code est vraiment volumineuse (disons plus de 100K LOC), suivre ces directives peut être très utile.

Si 5 millions de développeurs passent 15 minutes à apprendre une API plutôt que 60 minutes à l'apprendre, le résultat est une économie nette de 428 années-homme. Cela fait beaucoup de temps.

La plupart des projets, cependant, n'impliquent pas des millions de développeurs, ou 100K + LOC. Dans un projet typique, avec, disons 4 développeurs et environ 50K loc, l'ensemble des hypothèses est très différent. Les développeurs de l'équipe auront une bien meilleure compréhension du fonctionnement du code. Cela signifie qu'il est beaucoup plus judicieux d'optimiser pour produire rapidement du code de haute qualité et pour réduire le nombre de bogues et l'effort nécessaire pour apporter des modifications.

Passer 1 semaine à développer du code cohérent avec le framework .net, contre 8 heures à écrire du code facile à modifier et comportant moins de bogues, peut entraîner:

  1. Projets en retard
  2. Bonus moins élevés
  3. Augmentation du nombre de bogues
  4. Plus de temps passé au bureau et moins de temps sur la plage à boire des margaritas.

Sans 4 999 999 autres développeurs pour absorber les coûts, cela n'en vaut généralement pas la peine.

Par exemple, le test des interfaces de marqueur se résume à une seule expression «est» et entraîne moins de code que la recherche d'attributs.

Donc mon conseil est:

  1. Suivez religieusement les directives du cadre si vous développez des bibliothèques de classes (ou des widgets d'interface utilisateur) destinées à une consommation largement répandue.
  2. Envisagez d'en adopter certains si vous avez plus de 100K LOC dans votre projet
  3. Sinon, ignorez-les complètement.
Scott Wisniewski
la source
12
Personnellement, je vois tout code que j'écris en tant que bibliothèque que j'aurai besoin d'utiliser plus tard. Je ne me soucie pas vraiment de la consommation répandue ou non - suivre les directives augmente la cohérence et réduit la surprise lorsque j'ai besoin de regarder mon code et de le comprendre des années plus tard ...
Reed Copsey
16
Je ne dis pas que les directives sont mauvaises. Je dis qu'ils devraient être différents, en fonction de la taille de votre base de code et du nombre d'utilisateurs que vous avez. La plupart des directives de conception sont basées sur des choses comme le maintien de la comparabilité binaire, ce qui n'est pas aussi important pour les bibliothèques "internes" utilisées par une poignée de projets que pour quelque chose comme la BCL. D'autres directives, comme celles liées à la convivialité, sont presque toujours importantes. La morale est de ne pas être trop religieux sur les lignes directrices, en particulier sur les petits projets.
Scott Wisniewski
6
+1 - N'a pas tout à fait répondu à la question du PO - But de MI - Mais très utile quand même.
bzarah le
5
@ScottWisniewski: Je pense que vous manquez des points sérieux. Les lignes directrices du cadre ne s'appliquent tout simplement pas aux grands projets, elles s'appliquent aux projets de taille moyenne et à certains petits projets. Ils deviennent trop meurtriers lorsque vous essayez toujours de les appliquer au programme Hello-World. Par exemple, limiter les interfaces à 5 méthodes est toujours une bonne règle de base quelle que soit la taille de l'application. Une autre chose qui vous manque, la petite application d'aujourd'hui peut devenir la grande application de demain. Il est donc préférable de le construire en gardant à l'esprit les bons principes qui s'appliquent aux grandes applications afin que, lorsque vient le temps de passer à l'échelle, vous n'ayez pas à réécrire beaucoup de code.
Phil
2
Je ne vois pas vraiment comment le fait de suivre (la plupart) les directives de conception aboutirait à un projet de 8 heures prenant soudainement 1 semaine. ex: Nommer une virtual protectedméthode de modèle DoSomethingCoreau lieu de DoSomethingn'est pas beaucoup de travail supplémentaire et vous communiquez clairement qu'il s'agit d'une méthode de modèle ... IMNSHO, les personnes qui écrivent des applications sans tenir compte de l'API ( But.. I'm not a framework developer, I don't care about my API!) sont exactement ces personnes qui écrivent beaucoup de doublons ( et aussi du code non documenté et généralement illisible), et non l'inverse.
Laoujin
44

Les interfaces de marqueur sont utilisées pour marquer la capacité d'une classe comme implémentant une interface spécifique au moment de l'exécution.

Les directives de conception d'interface et de conception de type .NET - Conception d'interface découragent l'utilisation d'interfaces de marqueurs en faveur de l'utilisation d'attributs en C #, mais comme le souligne @Jay Bazuzi, il est plus facile de vérifier les interfaces de marqueurs que les attributs:o is I

Donc au lieu de ça:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

Les directives .NET vous ont recommandé de faire ceci:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 
Blé Mitch
la source
26
De plus, nous pouvons utiliser pleinement les génériques avec des interfaces de marqueurs, mais pas avec des attributs.
Jordão
18
Bien que j'adore les attributs et leur apparence d'un point de vue déclaratif, ils ne sont pas des citoyens de première classe au moment de l'exécution et nécessitent une quantité importante de plomberie de niveau relativement bas pour travailler.
Jesse C. Slicer
4
@ Jordão - C'était exactement ma pensée. Par exemple, si je veux faire abstraction du code d'accès à la base de données (disons Linq à SQL), avoir une interface commune le rend BEAUCOUP plus facile. En fait, je ne pense pas qu'il soit possible d'écrire ce genre d'abstraction avec des attributs puisque vous ne pouvez pas convertir un attribut et ne pouvez pas les utiliser dans les génériques. Je suppose que vous pouvez utiliser une classe de base vide dont dérivent toutes les autres classes, mais cela ressemble plus ou moins à une interface vide. De plus, si vous réalisez plus tard que vous avez besoin de fonctionnalités partagées, le mécanisme est déjà en place.
tandrewnichols
23

Étant donné que toutes les autres réponses ont déclaré "ils devraient être évités", il serait utile d'avoir une explication sur les raisons.

Premièrement, pourquoi les interfaces de marqueur sont utilisées: elles existent pour permettre au code qui utilise l'objet qui l'implémente de vérifier si elles implémentent ladite interface et de traiter l'objet différemment si c'est le cas.

Le problème avec cette approche est qu'elle rompt l'encapsulation. L'objet lui-même a désormais un contrôle indirect sur la façon dont il sera utilisé en externe. De plus, il connaît le système dans lequel il va être utilisé. En appliquant l'interface de marqueur, la définition de classe suggère qu'il s'attend à être utilisé quelque part qui vérifie l'existence du marqueur. Il a une connaissance implicite de l'environnement dans lequel il est utilisé et tente de définir comment il doit être utilisé. Cela va à l'encontre de l'idée d'encapsulation car il a connaissance de la mise en œuvre d'une partie du système qui existe entièrement en dehors de sa propre portée.

Au niveau pratique, cela réduit la portabilité et la réutilisabilité. Si la classe est réutilisée dans une autre application, l'interface doit également être copiée et elle peut ne pas avoir de sens dans le nouvel environnement, ce qui la rend entièrement redondante.

En tant que tel, le «marqueur» est des métadonnées sur la classe. Ces métadonnées ne sont pas utilisées par la classe elle-même et n'ont de sens que pour (certains!) Code client externe afin qu'il puisse traiter l'objet d'une certaine manière. Comme cela n'a de sens que pour le code client, les métadonnées doivent se trouver dans le code client, pas dans l'API de classe.

La différence entre une "interface de marqueur" et une interface normale est qu'une interface avec des méthodes dit au monde extérieur comment elle peut être utilisée alors qu'une interface vide implique qu'elle indique au monde extérieur comment elle doit être utilisée.

Tom B
la source
1
Le but principal de toute interface est de faire la distinction entre les classes qui promettent de respecter le contrat associé à cette interface et celles qui ne le font pas. Alors qu'une interface est également chargée de fournir les signatures d'appel de tous les membres nécessaires pour exécuter le contrat, c'est le contrat, plutôt que les membres, qui détermine si une interface particulière doit être implémentée par une classe particulière. Si le contrat pour IConstructableFromString<T>spécifie qu'une classe Tne peut implémenter que IConstructableFromString<T>si elle a un membre statique ...
supercat
... public static T ProduceFromString(String params);, une classe compagnon de l'interface peut offrir une méthode public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; si le code client avait une méthode comme T[] MakeManyThings<T>() where T:IConstructableFromString<T>, on pourrait définir de nouveaux types qui pourraient fonctionner avec le code client sans avoir à modifier le code client pour les gérer. Si les métadonnées se trouvaient dans le code client, il ne serait pas possible de créer de nouveaux types à utiliser par le client existant.
supercat du
Mais le contrat entre Tet la classe qui l'utilise est l' IConstructableFromString<T>endroit où vous avez une méthode dans l'interface qui décrit un comportement, donc ce n'est pas une interface de marqueur.
Tom B
La méthode statique que la classe doit avoir ne fait pas partie de l'interface. Les membres statiques des interfaces sont implémentés par les interfaces elles-mêmes; il n'y a aucun moyen pour une interface de faire référence à un membre statique dans une classe d'implémentation.
supercat
Il est possible pour une méthode de déterminer, à l'aide de Reflection, si un type générique possède une méthode statique particulière, et d'exécuter cette méthode si elle existe, mais le processus réel de recherche et d'exécution de la méthode statique ProduceFromStringdans l'exemple ci-dessus n'impliquerait pas l'interface de quelque manière que ce soit, sauf que l'interface serait utilisée comme marqueur pour indiquer quelles classes devraient être censées implémenter la fonction nécessaire.
supercat
8

Les interfaces de marqueurs peuvent parfois être un mal nécessaire lorsqu'un langage ne prend pas en charge les types d' union discriminés .

Supposons que vous souhaitiez définir une méthode qui attend un argument dont le type doit être exactement l'un parmi A, B ou C.Dans de nombreux langages fonctionnels (comme F # ), un tel type peut être clairement défini comme:

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

Cependant, dans les premiers langages OO tels que C #, cela n'est pas possible. La seule façon de réaliser quelque chose de similaire ici est de définir l'interface IArg et de "marquer" A, B et C avec elle.

Bien sûr, vous pourriez éviter d'utiliser l'interface de marqueur en acceptant simplement le type "objet" comme argument, mais vous perdriez alors l'expressivité et un certain degré de sécurité de type.

Les types d'unions discriminées sont extrêmement utiles et existent dans les langages fonctionnels depuis au moins 30 ans. Étrangement, à ce jour, tous les langages OO traditionnels ont ignoré cette fonctionnalité - même si elle n'a en fait rien à voir avec la programmation fonctionnelle en soi, mais appartient au système de types.

Marc Sigrist
la source
Il est intéressant de noter que comme a Foo<T>aura un ensemble séparé de champs statiques pour chaque type T, il n'est pas difficile d'avoir une classe générique contenant des champs statiques contenant des délégués pour traiter a T, et de pré-remplir ces champs avec des fonctions pour gérer chaque type de la classe est censé travailler avec. L'utilisation d'une contrainte d'interface générique sur le type Tvérifierait au moment du compilateur que le type fourni au moins prétendait être valide, même s'il ne serait pas en mesure de garantir qu'il l'était réellement.
supercat
6

Une interface de marqueur n'est qu'une interface vide. Une classe implémenterait cette interface sous forme de métadonnées à utiliser pour une raison quelconque. En C #, vous utiliseriez plus couramment des attributs pour baliser une classe pour les mêmes raisons que vous utiliseriez une interface de marqueur dans d'autres langages.

Richard Anthony Hein
la source
4

Une interface de marqueur permet à une classe d'être étiquetée d'une manière qui sera appliquée à toutes les classes descendantes. Une interface de marqueur "pure" ne définirait ni n'hériterait de quoi que ce soit; un type d'interface de marqueur plus utile peut être celui qui "hérite" d'une autre interface mais ne définit aucun nouveau membre. Par exemple, s'il y a une interface "IReadableFoo", on pourrait aussi définir une interface "IImmutableFoo", qui se comporterait comme un "Foo" mais promettrait à quiconque l'utilise que rien ne changerait sa valeur. Une routine qui accepte un IImmutableFoo serait capable de l'utiliser comme un IReadableFoo, mais la routine n'accepterait que les classes qui ont été déclarées comme implémentant IImmutableFoo.

Je ne peux pas penser à beaucoup d'utilisations pour les interfaces de marqueurs "pures". Le seul auquel je puisse penser serait si EqualityComparer (of T) .Default renverrait Object.Equals pour tout type qui implémentait IDoNotUseEqualityComparer, même si le type implémentait également IEqualityComparer. Cela permettrait d'avoir un type immuable non scellé sans violer le principe de substitution de Liskov: si le type scelle toutes les méthodes liées aux tests d'égalité, un type dérivé pourrait ajouter des champs supplémentaires et les rendre mutables, mais la mutation de ces champs ne le serait pas. t être visible en utilisant des méthodes de type de base. Il n'est peut-être pas horrible d'avoir une classe immuable non scellée et d'éviter toute utilisation d'EqualityComparer.Default ou de faire confiance aux classes dérivées pour ne pas implémenter IEqualityComparer,

supercat
la source
4

Ces deux méthodes d'extension résoudront la plupart des problèmes que Scott affirme en faveur des interfaces de marqueurs par rapport aux attributs:

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>()
}

Maintenant vous avez:

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

contre:

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

Je ne vois pas comment la construction d'une API prendra 5 fois plus de temps avec le premier modèle que le second, comme le prétend Scott.

Monsieur Anderson
la source
1
Toujours pas de génériques.
Ian Kemp
0

Les marqueurs sont des interfaces vides. Un marqueur est là ou non.

classe Foo: IConfidential

Ici, nous marquons Foo comme étant confidentiel. Aucune propriété ou attribut supplémentaire réel requis.

Rick O'Shea
la source
0

L'interface de marqueur est une interface vierge totale qui n'a pas de corps / données-membres / implémentation.
Une classe implémente une interface de marqueur lorsque cela est nécessaire, c'est juste pour " marquer "; signifie qu'il indique à la JVM que la classe particulière est destinée au clonage, alors permettez-lui de cloner. Cette classe particulière doit sérialiser ses objets, veuillez donc permettre à ses objets d'être sérialisés.

Arun Raaj
la source
0

L'interface de marqueur n'est en réalité qu'une programmation procédurale dans un langage OO. Une interface définit un contrat entre les implémenteurs et les consommateurs, sauf une interface de marqueur, car une interface de marqueur ne définit rien d'autre que lui-même. Ainsi, dès la sortie de la porte, l'interface de marqueur échoue dans le but fondamental d'être une interface.

Ali Bayat
la source