¿Cuál es el propósito de una interfaz de marcador?


Respuestas:


77

Esto es un poco tangente basado en la respuesta de "Mitch Wheat".

En general, cada vez que veo que la gente cita las pautas de diseño del marco, siempre me gusta mencionar que:

Por lo general, debe ignorar las pautas de diseño del marco la mayor parte del tiempo.

Esto no se debe a ningún problema con las pautas de diseño del marco. Creo que .NET Framework es una biblioteca de clases fantástica. Gran parte de esa fantasía proviene de las pautas de diseño del marco.

Sin embargo, las pautas de diseño no se aplican a la mayoría de los códigos escritos por la mayoría de los programadores. Su propósito es permitir la creación de un gran marco que sea utilizado por millones de desarrolladores, no hacer que la escritura de bibliotecas sea más eficiente.

Muchas de las sugerencias que contiene pueden guiarlo para hacer cosas que:

  1. Puede que no sea la forma más sencilla de implementar algo
  2. Puede resultar en duplicación de código adicional
  3. Puede tener una sobrecarga de tiempo de ejecución adicional

El framework .net es grande, realmente grande. Es tan grande que sería absolutamente irrazonable suponer que alguien tiene un conocimiento detallado de todos sus aspectos. De hecho, es mucho más seguro asumir que la mayoría de los programadores encuentran con frecuencia partes del marco que nunca antes habían usado.

En ese caso, los objetivos principales de un diseñador de API son:

  1. Mantenga las cosas consistentes con el resto del marco
  2. Elimina la complejidad innecesaria en la superficie de la API

Las pautas de diseño del marco empujan a los desarrolladores a crear código que cumpla con esos objetivos.

Eso significa hacer cosas como evitar capas de herencia, incluso si eso significa duplicar código, o empujar todas las excepciones arrojando código a "puntos de entrada" en lugar de usar ayudantes compartidos (para que los seguimientos de pila tengan más sentido en el depurador), y mucho de otras cosas similares.

La razón principal por la que esas pautas sugieren el uso de atributos en lugar de interfaces de marcador es porque eliminar las interfaces de marcador hace que la estructura de herencia de la biblioteca de clases sea mucho más accesible. Un diagrama de clases con 30 tipos y 6 capas de jerarquía de herencia es muy desalentador en comparación con uno con 15 tipos y 2 capas de jerarquía.

Si realmente hay millones de desarrolladores que usan sus API, o su base de código es realmente grande (digamos más de 100K LOC), seguir esas pautas puede ayudar mucho.

Si 5 millones de desarrolladores dedican 15 minutos a aprender una API en lugar de dedicar 60 minutos a aprenderla, el resultado es un ahorro neto de 428 años-hombre. Eso es mucho tiempo.

La mayoría de los proyectos, sin embargo, no involucran a millones de desarrolladores o 100K + LOC. En un proyecto típico, con 4 desarrolladores y alrededor de 50K locomotoras, el conjunto de suposiciones es muy diferente. Los desarrolladores del equipo comprenderán mucho mejor cómo funciona el código. Eso significa que tiene mucho más sentido optimizar para producir código de alta calidad rápidamente y para reducir la cantidad de errores y el esfuerzo necesario para realizar cambios.

Pasar 1 semana desarrollando código que sea consistente con el marco .net, en comparación con 8 horas escribiendo código que sea fácil de cambiar y tenga menos errores puede resultar en:

  1. Proyectos tardíos
  2. Bonificaciones más bajas
  3. Mayor número de errores
  4. Más tiempo en la oficina y menos tiempo en la playa bebiendo margaritas.

Sin 4.999.999 otros desarrolladores para absorber los costos, generalmente no vale la pena.

Por ejemplo, la prueba de interfaces de marcador se reduce a una sola expresión "es" y da como resultado menos código que la búsqueda de atributos.

Entonces mi consejo es:

  1. Siga las pautas del marco religiosamente si está desarrollando bibliotecas de clases (o widgets de interfaz de usuario) destinadas a un consumo generalizado.
  2. Considere adoptar algunos de ellos si tiene más de 100K LOC en su proyecto
  3. De lo contrario, ignórelos por completo.

12
Yo, personalmente, veo cualquier código que escribo como una biblioteca que necesitaré usar más adelante. Realmente no me importa si el consumo está generalizado o no: seguir las pautas aumenta la coherencia y reduce la sorpresa cuando necesito ver mi código y entenderlo años después ...
Reed Copsey

16
No digo que las pautas sean malas. Estoy diciendo que deberían ser diferentes, dependiendo del tamaño de su base de código y la cantidad de usuarios que tenga. Muchas de las pautas de diseño se basan en cosas como mantener la comparabilidad binaria, que no es tan importante para las bibliotecas "internas" utilizadas por un puñado de proyectos como lo es para algo como el BCL. Otras pautas, como las relacionadas con la usabilidad, casi siempre son importantes. La moraleja es no ser demasiado religioso acerca de las pautas, particularmente en proyectos pequeños.
Scott Wisniewski

6
+1 - No respondió bien la pregunta del OP - Propósito de MI - Pero de todos modos fue muy útil.
bzarah

5
@ScottWisniewski: Creo que le faltan puntos importantes. Las directrices del Marco simplemente no se aplican a proyectos grandes, se aplican a proyectos medianos y pequeños. Se vuelven excesivos cuando siempre intenta aplicarlos al programa Hello-World. Por ejemplo, limitar las interfaces a 5 métodos siempre es una buena regla, independientemente del tamaño de la aplicación. Otra cosa que extrañas, la pequeña aplicación de hoy puede convertirse en la gran aplicación del mañana. Por lo tanto, es mejor que lo construya con buenos principios que se apliquen a aplicaciones grandes en mente para que cuando llegue el momento de escalar, no tenga que volver a escribir mucho código.
Phil

2
No veo muy bien cómo seguir (la mayoría de) las pautas de diseño resultaría en un proyecto de 8 horas que de repente tomaría 1 semana. Ej: nombrar un virtual protectedmétodo de plantilla en DoSomethingCorelugar de DoSomethingno es mucho trabajo adicional y usted comunica claramente que es un método de plantilla ... IMNSHO, las personas que escriben aplicaciones sin considerar la API ( But.. I'm not a framework developer, I don't care about my API!) son exactamente aquellas personas que escriben muchos duplicados ( y también código indocumentado y generalmente ilegible), no al revés.
Laoujin

45

Las interfaces de marcador se utilizan para marcar la capacidad de una clase como implementación de una interfaz específica en tiempo de ejecución.

Las Pautas de Diseño de Interfaz y Diseño de Tipo .NET - Diseño de Interfaz desalientan el uso de interfaces de marcador a favor del uso de atributos en C #, pero como señala @Jay Bazuzi, es más fácil verificar las interfaces de marcador que los atributos:o is I

Entonces en lugar de esto:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

Las pautas de .NET recomiendan que haga esto:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 

27
Además, podemos usar genéricos con interfaces de marcador, pero no con atributos.
Jordão

18
Si bien me encantan los atributos y cómo se ven desde un punto de vista declarativo, no son ciudadanos de primera clase en tiempo de ejecución y requieren una cantidad significativa de plomería de nivel relativamente bajo para trabajar.
Jesse C. Slicer

4
@ Jordão - Este fue mi pensamiento exactamente. Como ejemplo, si quiero abstraer el código de acceso a la base de datos (digamos Linq a Sql), tener una interfaz común lo hace MUCHO más fácil. De hecho, no creo que sea posible escribir ese tipo de abstracción con atributos, ya que no se puede convertir a un atributo y no se pueden usar en genéricos. Supongo que podría usar una clase base vacía de la que derivan todas las otras clases, pero eso se siente más o menos lo mismo que tener una interfaz vacía. Además, si luego se da cuenta de que necesita una funcionalidad compartida, el mecanismo ya está implementado.
tandrewnichols

23

Dado que todas las demás respuestas han dicho "deberían evitarse", sería útil tener una explicación de por qué.

En primer lugar, por qué se usan las interfaces de marcador: existen para permitir que el código que está usando el objeto que lo implementa compruebe si implementa dicha interfaz y trata el objeto de manera diferente si lo hace.

El problema con este enfoque es que rompe la encapsulación. El objeto en sí tiene ahora control indirecto sobre cómo se utilizará externamente. Además, tiene conocimiento del sistema en el que se va a utilizar. Al aplicar la interfaz del marcador, la definición de clase sugiere que espera ser utilizada en algún lugar que compruebe la existencia del marcador. Tiene un conocimiento implícito del entorno en el que se usa y está tratando de definir cómo se debe usar. Esto va en contra de la idea de encapsulación porque tiene conocimiento de la implementación de una parte del sistema que existe completamente fuera de su propio alcance.

A nivel práctico, esto reduce la portabilidad y la reutilización. Si la clase se reutiliza en una aplicación diferente, la interfaz también debe copiarse y es posible que no tenga ningún significado en el nuevo entorno, por lo que es completamente redundante.

Como tal, el "marcador" son metadatos sobre la clase. Estos metadatos no son utilizados por la clase en sí y solo son significativos para (¡algunos!) Código de cliente externo para que pueda tratar el objeto de cierta manera. Debido a que solo tiene significado para el código del cliente, los metadatos deben estar en el código del cliente, no en la API de la clase.

La diferencia entre una "interfaz de marcador" y una interfaz normal es que una interfaz con métodos le dice al mundo exterior cómo se puede usar, mientras que una interfaz vacía implica que le dice al mundo exterior cómo debe usarse.


1
El propósito principal de cualquier interfaz es distinguir entre las clases que prometen cumplir con el contrato asociado con esa interfaz y las que no lo hacen. Si bien una interfaz también es responsable de proporcionar las firmas de llamada de los miembros necesarios para cumplir con el contrato, es el contrato, más que los miembros, el que determina si una interfaz en particular debe ser implementada por una clase en particular. Si el contrato IConstructableFromString<T>especifica que una clase Tsolo puede implementarse IConstructableFromString<T>si tiene un miembro estático ...
supercat

... public static T ProduceFromString(String params);, una clase complementaria a la interfaz puede ofrecer un método public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; si el código del cliente tuviera un método como T[] MakeManyThings<T>() where T:IConstructableFromString<T>, se podrían definir nuevos tipos que podrían funcionar con el código del cliente sin tener que modificar el código del cliente para tratar con ellos. Si los metadatos estuvieran en el código del cliente, no sería posible crear nuevos tipos para que los utilice el cliente existente.
supercat

Pero el contrato entre Ty la clase que lo usa es IConstructableFromString<T>donde tienes un método en la interfaz que describe algún comportamiento, por lo que no es una interfaz de marcador.
Tom B

El método estático que debe tener la clase no es parte de la interfaz. Los miembros estáticos de las interfaces son implementados por las propias interfaces; no hay forma de que una interfaz se refiera a un miembro estático en una clase de implementación.
supercat

Es posible que un método determine, usando Reflection, si un tipo genérico tiene un método estático en particular y ejecutar ese método si existe, pero el proceso real de búsqueda y ejecución del método estático ProduceFromStringen el ejemplo anterior no implicaría la interfaz de cualquier manera, excepto que la interfaz se usaría como un marcador para indicar qué clases se espera que implementen la función necesaria.
supercat

8

Las interfaces de marcador a veces pueden ser un mal necesario cuando un idioma no admite tipos de unión discriminados .

Suponga que desea definir un método que espera un argumento cuyo tipo debe ser exactamente uno de A, B o C. En muchos lenguajes funcionales primero (como F # ), dicho tipo se puede definir claramente como:

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

Sin embargo, en los primeros lenguajes de OO como C #, esto no es posible. La única forma de lograr algo similar aquí es definir la interfaz IArg y "marcar" A, B y C con ella.

Por supuesto, podría evitar el uso de la interfaz del marcador simplemente aceptando el tipo "objeto" como argumento, pero entonces perdería expresividad y cierto grado de seguridad de tipos.

Los tipos de sindicatos discriminados son extremadamente útiles y han existido en lenguajes funcionales durante al menos 30 años. Curiosamente, hasta el día de hoy, todos los lenguajes OO convencionales han ignorado esta característica, aunque en realidad no tiene nada que ver con la programación funcional en sí, sino que pertenece al sistema de tipos.


Vale la pena señalar que debido a Foo<T>que a tendrá un conjunto separado de campos estáticos para cada tipo T, no es difícil tener una clase genérica que contenga campos estáticos que contengan delegados para procesar a T, y rellenar previamente esos campos con funciones para manejar todos los tipos de la clase. se supone que debe trabajar. El uso de una restricción de interfaz genérica sobre el tipo Tcomprobaría en el momento del compilador que el tipo proporcionado al menos afirmaba ser válido, aunque no podría garantizar que realmente lo fuera.
supercat

6

Una interfaz de marcador es solo una interfaz que está vacía. Una clase implementaría esta interfaz como metadatos para ser usados ​​por alguna razón. En C #, usaría más comúnmente atributos para marcar una clase por las mismas razones por las que usaría una interfaz de marcador en otros idiomas.


4

Una interfaz de marcador permite etiquetar una clase de una manera que se aplicará a todas las clases descendientes. Una interfaz de marcador "pura" no definiría ni heredaría nada; Un tipo más útil de interfaces de marcador puede ser uno que "hereda" otra interfaz pero no define nuevos miembros. Por ejemplo, si hay una interfaz "IReadableFoo", también se podría definir una interfaz "IImmutableFoo", que se comportaría como un "Foo" pero prometería a cualquiera que la use que nada cambiaría su valor. Una rutina que acepta un IImmutableFoo podría usarlo como lo haría un IReadableFoo, pero la rutina solo aceptaría clases que fueron declaradas como implementadoras de IImmutableFoo.

No puedo pensar en muchos usos para las interfaces de marcador "puro". El único en el que puedo pensar sería si EqualityComparer (of T) .Default devolvería Object.Equals para cualquier tipo que implementó IDoNotUseEqualityComparer, incluso si el tipo también implementó IEqualityComparer. Esto permitiría tener un tipo inmutable sin sellar sin violar el principio de sustitución de Liskov: si el tipo sella todos los métodos relacionados con la prueba de igualdad, un tipo derivado podría agregar campos adicionales y hacer que sean mutables, pero la mutación de tales campos no lo haría ' t ser visible utilizando cualquier método de tipo base. Puede que no sea horrible tener una clase inmutable sin sellar y evitar cualquier uso de EqualityComparer.Default o confiar en las clases derivadas para no implementar IEqualityComparer,


4

Estos dos métodos de extensión resolverán la mayoría de los problemas que Scott afirma que favorecen las interfaces de marcador sobre los atributos:

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

Ahora tu tienes:

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

versus:

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

No veo cómo la construcción de una API llevará 5 veces más tiempo con el primer patrón en comparación con el segundo, como afirma Scott.


1
Todavía no hay genéricos.
Ian Kemp

1

Los marcadores son interfaces vacías. Un marcador está ahí o no lo está.

clase Foo: IConfidential

Aquí marcamos a Foo como confidencial. No se requieren propiedades o atributos adicionales reales.


0

La interfaz de marcador es una interfaz en blanco total que no tiene cuerpo / miembros de datos / implementación.
Una clase implementa una interfaz de marcador cuando se requiere, es solo para " marcar "; significa que le dice a la JVM que la clase en particular tiene el propósito de clonar, así que permita que se clone. Esta clase en particular es para serializar sus objetos, así que permita que sus objetos se serialicen.


0

La interfaz del marcador es en realidad solo una programación de procedimiento en un lenguaje OO. Una interfaz define un contrato entre implementadores y consumidores, excepto una interfaz de marcador, porque una interfaz de marcador no define nada más que a sí misma. Entonces, desde el principio, la interfaz del marcador falla en el propósito básico de ser una interfaz.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.