Qual é a finalidade de uma interface de marcador?


Respostas:


77

Isso é um pouco tangente com base na resposta de "Mitch Wheat".

Geralmente, sempre que vejo pessoas citarem as diretrizes de design da estrutura, sempre gosto de mencionar que:

Geralmente, você deve ignorar as diretrizes de design da estrutura na maioria das vezes.

Isso não é devido a qualquer problema com as diretrizes de design da estrutura. Acho que o .NET framework é uma biblioteca de classes fantástica. Muito desse fantástico flui das diretrizes de design da estrutura.

No entanto, as diretrizes de design não se aplicam à maioria dos códigos escritos pela maioria dos programadores. Seu objetivo é permitir a criação de uma grande estrutura usada por milhões de desenvolvedores, não para tornar a escrita da biblioteca mais eficiente.

Muitas das sugestões nele podem orientá-lo a fazer coisas que:

  1. Pode não ser a maneira mais direta de implementar algo
  2. Pode resultar em duplicação de código extra
  3. Pode ter sobrecarga de tempo de execução extra

A estrutura .net é grande, muito grande. É tão grande que seria absolutamente irracional supor que alguém tenha conhecimento detalhado sobre cada aspecto dele. Na verdade, é muito mais seguro presumir que a maioria dos programadores frequentemente encontra partes da estrutura que nunca usaram antes.

Nesse caso, os objetivos principais de um designer de API são:

  1. Mantenha as coisas consistentes com o resto da estrutura
  2. Elimine a complexidade desnecessária na área de superfície da API

As diretrizes de design da estrutura levam os desenvolvedores a criar um código que atenda a esses objetivos.

Isso significa fazer coisas como evitar camadas de herança, mesmo que signifique duplicar o código, ou enviar todas as exceções lançando o código para "pontos de entrada" em vez de usar ajudantes compartilhados (para que os rastreamentos de pilha façam mais sentido no depurador), e muito de outras coisas semelhantes.

O principal motivo pelo qual essas diretrizes sugerem o uso de atributos em vez de interfaces de marcador é porque a remoção das interfaces de marcador torna a estrutura de herança da biblioteca de classes muito mais acessível. Um diagrama de classes com 30 tipos e 6 camadas de hierarquia de herança é muito assustador em comparação com outro com 15 tipos e 2 camadas de hierarquia.

Se realmente houver milhões de desenvolvedores usando suas APIs ou se sua base de código for muito grande (digamos, mais de 100K LOC), seguir essas diretrizes pode ajudar muito.

Se 5 milhões de desenvolvedores gastam 15 minutos aprendendo uma API em vez de 60 minutos aprendendo, o resultado é uma economia líquida de 428 anos-homem. É muito tempo.

A maioria dos projetos, entretanto, não envolve milhões de desenvolvedores, ou 100K + LOC. Em um projeto típico, digamos com 4 desenvolvedores e cerca de 50K loc, o conjunto de premissas é muito diferente. Os desenvolvedores da equipe terão uma compreensão muito melhor de como o código funciona. Isso significa que faz muito mais sentido otimizar para produzir código de alta qualidade rapidamente e reduzir a quantidade de bugs e o esforço necessário para fazer alterações.

Gastar 1 semana desenvolvendo código consistente com a estrutura .net, em comparação com 8 horas escrevendo código fácil de alterar e com menos bugs pode resultar em:

  1. Projetos atrasados
  2. Bônus mais baixos
  3. Maior contagem de bugs
  4. Mais tempo no escritório e menos tempo na praia bebendo margaritas.

Sem 4.999.999 outros desenvolvedores para absorver os custos, geralmente não vale a pena.

Por exemplo, o teste de interfaces de marcador se resume a uma única expressão "é" e resulta em menos código do que a procura de atributos.

Portanto, meu conselho é:

  1. Siga as diretrizes da estrutura religiosamente se estiver desenvolvendo bibliotecas de classes (ou widgets de IU) destinadas ao consumo generalizado.
  2. Considere a adoção de alguns deles se você tiver mais de 100K LOC em seu projeto
  3. Caso contrário, ignore-os completamente.

12
Eu, pessoalmente, vejo qualquer código que escrevo como uma biblioteca que precisarei usar mais tarde. Eu realmente não me importo se o consumo é generalizado ou não - seguir as diretrizes aumenta a consistência e reduz a surpresa quando preciso olhar meu código e entendê-lo anos depois ...
Reed Copsey

16
Não estou dizendo que as diretrizes são ruins. Estou dizendo que eles deveriam ser diferentes, dependendo do tamanho de sua base de código e do número de usuários que você tem. Muitas das diretrizes de design são baseadas em coisas como manter a comparabilidade binária, o que não é tão importante para bibliotecas "internas" usadas por um punhado de projetos como é para algo como o BCL. Outras diretrizes, como as relacionadas à usabilidade, quase sempre são importantes. A moral é não ser excessivamente religioso sobre as diretrizes, especialmente em pequenos projetos.
Scott Wisniewski

6
+1 - Não respondeu bem a pergunta do OP - Objetivo do MI - Mas, mesmo assim, foi muito útil.
bzarah

5
@ScottWisniewski: Acho que você está perdendo pontos importantes. As diretrizes do Framework simplesmente não se aplicam a projetos grandes, mas sim a projetos médios e alguns pequenos. Eles se tornam exagerados quando você sempre tenta aplicá-los ao programa Hello-World. Por exemplo, limitar as interfaces a 5 métodos é sempre uma boa regra, independentemente do tamanho do aplicativo. Outra coisa que você sente falta: o pequeno aplicativo de hoje pode se tornar o grande aplicativo de amanhã. Portanto, é melhor você construí-lo com bons princípios que se aplicam a aplicativos grandes em mente, de modo que, quando chegar a hora de aumentar a escala, você não precise reescrever muitos códigos.
Phil

2
Não vejo bem como seguir (a maior parte) as diretrizes de design resultaria em um projeto de 8 horas de repente levando 1 semana. ex: Nomear um virtual protectedmétodo de modelo em DoSomethingCorevez de DoSomethingnão é muito trabalho adicional e você comunica claramente que é um método de modelo ... IMNSHO, as pessoas que escrevem aplicativos sem considerar a API ( But.. I'm not a framework developer, I don't care about my API!) são exatamente aquelas pessoas que escrevem muitos duplicados ( e também código não documentado e geralmente ilegível, e não o contrário.
Laoujin

44

Marker Interfaces são usadas para marcar a capacidade de uma classe de implementação de uma interface específica em tempo de execução.

As Diretrizes de Design de Interface e de Design de Tipo .NET - Design de Interface desencorajam o uso de interfaces de marcador em favor do uso de atributos em C #, mas como @Jay Bazuzi aponta, é mais fácil verificar interfaces de marcador do que atributos:o is I

Então, em vez disso:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

As diretrizes do .NET recomendam que você faça isso:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 

26
Além disso, podemos usar totalmente os genéricos com interfaces de marcador, mas não com atributos.
Jordão

18
Embora eu ame atributos e sua aparência do ponto de vista declarativo, eles não são cidadãos de primeira classe em tempo de execução e requerem uma quantidade significativa de encanamento de nível relativamente baixo para trabalhar.
Jesse C. Slicer

4
@ Jordão - Foi exatamente esse o meu pensamento. Por exemplo, se eu quiser abstrair o código de acesso ao banco de dados (digamos Linq para Sql), ter uma interface comum torna MUITO mais fácil. Na verdade, não acho que seria possível escrever esse tipo de abstração com atributos, já que você não pode lançar para um atributo e não pode usá-los em genéricos. Suponho que você possa usar uma classe base vazia da qual todas as outras classes derivam, mas isso parece mais ou menos o mesmo que ter uma interface vazia. Além disso, se mais tarde você perceber que precisa de funcionalidade compartilhada, o mecanismo já está em vigor.
tandrewnichols

23

Uma vez que todas as outras respostas afirmam "eles devem ser evitados", seria útil ter uma explicação do porquê.

Em primeiro lugar, por que as interfaces de marcador são usadas: elas existem para permitir que o código que está usando o objeto que o implementa verifique se eles implementam essa interface e tratem o objeto de forma diferente, se o fizer.

O problema com essa abordagem é que ela quebra o encapsulamento. O próprio objeto agora tem controle indireto sobre como será usado externamente. Além disso, ele tem conhecimento do sistema em que será usado. Ao aplicar a interface do marcador, a definição da classe sugere que ela espera ser usada em algum lugar que verifique a existência do marcador. Ele tem conhecimento implícito do ambiente em que é usado e está tentando definir como deve ser usado. Isso vai contra a ideia de encapsulamento porque tem conhecimento da implementação de uma parte do sistema que existe inteiramente fora de seu próprio escopo.

Em um nível prático, isso reduz a portabilidade e a reutilização. Se a classe for reutilizada em um aplicativo diferente, a interface também precisa ser copiada e pode não ter nenhum significado no novo ambiente, tornando-a totalmente redundante.

Como tal, o "marcador" são metadados sobre a classe. Esses metadados não são usados ​​pela própria classe e são significativos apenas para (alguns!) Códigos de cliente externos, de forma que possam tratar o objeto de uma determinada maneira. Por ter significado apenas para o código do cliente, os metadados devem estar no código do cliente, não na API da classe.

A diferença entre uma "interface de marcador" e uma interface normal é que uma interface com métodos informa ao mundo externo como ela pode ser usada, enquanto uma interface vazia indica que está dizendo ao mundo externo como deve ser usada.


1
O objetivo principal de qualquer interface é distinguir entre as classes que prometem cumprir o contrato associado a essa interface e as que não o fazem. Embora uma interface também seja responsável por fornecer as assinaturas de chamada de quaisquer membros necessários para cumprir o contrato, é o contrato, e não os membros, que determina se uma interface específica deve ser implementada por uma classe específica. Se o contrato para IConstructableFromString<T>especifica que uma classe Tsó pode implementar IConstructableFromString<T>se tiver um membro estático ...
supercat

... public static T ProduceFromString(String params);, uma classe complementar à interface pode oferecer um método public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; se o código do cliente tivesse um método como T[] MakeManyThings<T>() where T:IConstructableFromString<T>, seria possível definir novos tipos que poderiam funcionar com o código do cliente sem ter que modificar o código do cliente para lidar com eles. Se os metadados estivessem no código do cliente, não seria possível criar novos tipos para uso pelo cliente existente.
supercat

Mas o contrato entre Te a classe que o usa é IConstructableFromString<T>onde você tem um método na interface que descreve algum comportamento, então não é uma interface de marcador.
Tom B

O método estático que a classe deve ter não faz parte da interface. Membros estáticos em interfaces são implementados pelas próprias interfaces; não há como uma interface referir-se a um membro estático em uma classe de implementação.
supercat

É possível para um método determinar, usando Reflection, se um tipo genérico por acaso tem um método estático específico e executar esse método se ele existir, mas o processo real de pesquisar e executar o método estático ProduceFromStringno exemplo acima não envolveria a interface de qualquer forma, exceto que a interface seria usada como um marcador para indicar quais classes deveriam implementar a função necessária.
supercat

8

As interfaces de marcador podem às vezes ser um mal necessário quando um idioma não suporta tipos de união discriminados .

Suponha que você queira definir um método que espera um argumento cujo tipo deve ser exatamente um de A, B ou C. Em muitas linguagens funcionais primeiro (como F # ), tal tipo pode ser claramente definido como:

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

No entanto, em linguagens OO-first, como C #, isso não é possível. A única maneira de conseguir algo semelhante aqui é definir a interface IArg e "marcar" A, B e C com ela.

Claro, você poderia evitar o uso da interface do marcador simplesmente aceitando o tipo "objeto" como argumento, mas então você perderia expressividade e algum grau de segurança de tipo.

Os tipos de união discriminados são extremamente úteis e existem nas linguagens funcionais há pelo menos 30 anos. Estranhamente, até hoje, todas as principais linguagens OO ignoraram esse recurso - embora na verdade não tenha nada a ver com a programação funcional em si, mas pertença ao sistema de tipos.


É importante notar que, como um Foo<T>terá um conjunto separado de campos estáticos para cada tipo T, não é difícil ter uma classe genérica contendo campos estáticos contendo delegados para processar um Te pré-preencher esses campos com funções para lidar com cada tipo que a classe é suposto trabalhar. O uso de uma restrição de interface genérica sobre o tipo Tverificaria, no momento do compilador, se o tipo fornecido pelo menos afirmava ser válido, embora não fosse capaz de garantir que realmente fosse.
supercat de

6

Uma interface de marcador é apenas uma interface vazia. Uma classe implementaria essa interface como metadados a serem usados ​​por algum motivo. Em C #, você usaria mais comumente atributos para marcar uma classe pelos mesmos motivos que usaria uma interface de marcador em outras linguagens.


4

Uma interface de marcador permite que uma classe seja marcada de uma forma que será aplicada a todas as classes descendentes. Uma interface de marcador "pura" não definiria ou herdaria nada; um tipo mais útil de interfaces de marcador pode ser aquele que "herda" outra interface, mas não define novos membros. Por exemplo, se houver uma interface "IReadableFoo", também se pode definir uma interface "IImmutableFoo", que se comportaria como um "Foo", mas prometeria a quem a usasse que nada mudaria seu valor. Uma rotina que aceita um IImmutableFoo seria capaz de usá-lo como faria com um IReadableFoo, mas a rotina só aceitaria classes que foram declaradas como implementando IImmutableFoo.

Não consigo pensar em muitos usos para interfaces de marcador "puras". O único em que consigo pensar seria se EqualityComparer (de T) .Default retornaria Object.Equals para qualquer tipo que implementasse IDoNotUseEqualityComparer, mesmo se o tipo também implementasse IEqualityComparer. Isso permitiria ter um tipo imutável não lacrado sem violar o Princípio de Substituição de Liskov: se o tipo selar todos os métodos relacionados ao teste de igualdade, um tipo derivado poderia adicionar campos adicionais e torná-los mutáveis, mas a mutação de tais campos não seria t ser visível usando qualquer método do tipo base. Pode não ser horrível ter uma classe imutável sem lacre e evitar qualquer uso de EqualityComparer.Default ou classes derivadas de confiança para não implementar IEqualityComparer,


4

Esses dois métodos de extensão resolverão a maioria dos problemas que Scott afirma favorecer interfaces de marcador em vez de 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>()
}

Agora você tem:

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

versus:

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

Não consigo ver como construir uma API levará 5 vezes mais tempo com o primeiro padrão em comparação com o segundo, como afirma Scott.


1
Ainda sem genéricos.
Ian Kemp

0

Os marcadores são interfaces vazias. Um marcador está lá ou não está.

classe Foo: IConfidential

Aqui, marcamos Foo como confidencial. Não são necessárias propriedades ou atributos adicionais reais.


0

A interface do marcador é uma interface totalmente em branco que não possui corpo / membros de dados / implementação.
Uma classe implementa interface de marcador quando necessário, é apenas para " marcar "; significa que ele informa à JVM que a classe específica é para fins de clonagem, portanto, permita a sua clonagem. Esta classe específica serve para serializar seus objetos, portanto, permita que seus objetos sejam serializados.


0

A interface do marcador é realmente apenas uma programação procedural em uma linguagem OO. Uma interface define um contrato entre implementadores e consumidores, exceto uma interface de marcador, porque uma interface de marcador define nada além de si mesma. Assim, logo de cara, a interface do marcador falha no propósito básico de ser uma interface.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.