À quoi sert une interface de marqueurs?
Réponses:
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:
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:
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:
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:
virtual protected
méthode de modèle DoSomethingCore
au lieu de DoSomething
n'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.
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
{
...
}
É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.
IConstructableFromString<T>
spécifie qu'une classe T
ne peut implémenter que IConstructableFromString<T>
si elle a un membre statique ...
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.
T
et 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.
ProduceFromString
dans 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.
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.
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 T
vé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.
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.
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,
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.
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.
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.
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.