Qual è lo scopo di un'interfaccia marker?


Risposte:


77

Questa è un po 'una tangente basata sulla risposta di "Mitch Wheat".

In generale, ogni volta che vedo persone che citano le linee guida per la progettazione del framework, mi piace sempre menzionarlo:

In genere dovresti ignorare le linee guida per la progettazione del framework la maggior parte delle volte.

Ciò non è dovuto a problemi con le linee guida per la progettazione del framework. Penso che .NET Framework sia una fantastica libreria di classi. Molte di queste fantasie derivano dalle linee guida di progettazione del framework.

Tuttavia, le linee guida di progettazione non si applicano alla maggior parte del codice scritto dalla maggior parte dei programmatori. Il loro scopo è consentire la creazione di un framework di grandi dimensioni che viene utilizzato da milioni di sviluppatori, non per rendere più efficiente la scrittura di librerie.

Molti dei suggerimenti in esso contenuti possono guidarti a fare cose che:

  1. Potrebbe non essere il modo più semplice per implementare qualcosa
  2. Potrebbe causare una duplicazione del codice aggiuntiva
  3. Potrebbe avere un sovraccarico di runtime aggiuntivo

Il framework .net è grande, davvero grande. È così grande che sarebbe assolutamente irragionevole presumere che qualcuno abbia una conoscenza dettagliata di ogni aspetto di esso. In effetti, è molto più sicuro presumere che la maggior parte dei programmatori incontri frequentemente parti del framework che non hanno mai utilizzato prima.

In tal caso, gli obiettivi principali di un progettista API sono:

  1. Mantieni le cose coerenti con il resto del framework
  2. Elimina la complessità non necessaria nell'area della superficie API

Le linee guida per la progettazione del framework spingono gli sviluppatori a creare codice che raggiunga questi obiettivi.

Ciò significa fare cose come evitare livelli di ereditarietà, anche se significa duplicare il codice, o spingere tutte le eccezioni che lanciano il codice ai "punti di ingresso" piuttosto che usare helper condivisi (in modo che le tracce dello stack abbiano più senso nel debugger), e molto di altre cose simili.

La ragione principale per cui queste linee guida suggeriscono di usare attributi invece di interfacce marker è perché la rimozione delle interfacce marker rende la struttura di ereditarietà della libreria di classi molto più accessibile. Un diagramma delle classi con 30 tipi e 6 livelli di gerarchia di ereditarietà è molto scoraggiante rispetto a uno con 15 tipi e 2 livelli di gerarchia.

Se ci sono davvero milioni di sviluppatori che utilizzano le tue API, o la tua base di codice è davvero grande (diciamo oltre 100.000 LOC), seguire queste linee guida può aiutare molto.

Se 5 milioni di sviluppatori dedicano 15 minuti all'apprendimento di un'API anziché 60 minuti all'apprendimento, il risultato è un risparmio netto di 428 anni uomo. È un sacco di tempo.

La maggior parte dei progetti, tuttavia, non coinvolge milioni di sviluppatori o 100K + LOC. In un progetto tipico, con diciamo 4 sviluppatori e circa 50.000 loc, l'insieme di ipotesi è molto diverso. Gli sviluppatori del team avranno una migliore comprensione di come funziona il codice. Ciò significa che ha molto più senso ottimizzare per produrre rapidamente codice di alta qualità e per ridurre la quantità di bug e lo sforzo necessario per apportare modifiche.

Trascorrere 1 settimana per sviluppare codice coerente con il framework .net, invece di 8 ore per scrivere codice facile da modificare e con meno bug, può comportare:

  1. Progetti in ritardo
  2. Bonus inferiori
  3. Conteggio dei bug aumentato
  4. Più tempo trascorso in ufficio e meno tempo in spiaggia a bere margarita.

Senza 4.999.999 altri sviluppatori per assorbire i costi, di solito non ne vale la pena.

Ad esempio, il test per le interfacce dei marker si riduce a una singola espressione "is" e produce meno codice rispetto alla ricerca di attributi.

Quindi il mio consiglio è:

  1. Segui religiosamente le linee guida del framework se stai sviluppando librerie di classi (o widget dell'interfaccia utente) pensate per un consumo diffuso.
  2. Valuta di adottarne alcuni se hai più di 100.000 LOC nel tuo progetto
  3. Altrimenti ignorali completamente.

12
Personalmente, vedo qualsiasi codice che scrivo come una libreria che dovrò utilizzare in seguito. Non mi interessa molto il consumo è diffuso o meno - seguire le linee guida aumenta la coerenza e riduce la sorpresa quando ho bisogno di guardare il mio codice e capirlo anni dopo ...
Reed Copsey

16
Non sto dicendo che le linee guida sono cattive. Sto dicendo che dovrebbero essere diversi, a seconda delle dimensioni del codice di base e del numero di utenti che hai. Molte delle linee guida di progettazione si basano su cose come il mantenimento della comparabilità binaria, che non è così importante per le librerie "interne" usate da una manciata di progetti come lo è per qualcosa come il BCL. Altre linee guida, come quelle relative all'usabilità, sono quasi sempre importanti. La morale è di non essere eccessivamente religiosi sulle linee guida, in particolare sui piccoli progetti.
Scott Wisniewski,

6
+1 - Non ha risposto del tutto alla domanda dell'OP - Scopo del MI - Ma comunque molto utile.
bzarah

5
@ScottWisniewski: Penso che ti manchino punti seri. Le linee guida del Framework non si applicano a progetti di grandi dimensioni, si applicano a progetti medi e alcuni piccoli. Diventano esagerati quando provi sempre ad applicarli al programma Hello-World. Ad esempio, limitare le interfacce a 5 metodi è sempre una buona regola pratica indipendentemente dalle dimensioni dell'app. Un'altra cosa che ti manca, la piccola app di oggi potrebbe diventare la grande app di domani. Quindi, è meglio che tu lo crei tenendo a mente i buoni principi che si applicano alle app di grandi dimensioni in modo che quando arriva il momento di scalare, non devi riscrivere molto codice.
Phil

2
Non vedo bene come seguendo (la maggior parte delle) le linee guida di progettazione risulterebbe un progetto di 8 ore che impiega improvvisamente 1 settimana. es: Denominare un virtual protectedmetodo modello DoSomethingCoreinvece di DoSomethingnon è molto lavoro aggiuntivo e comunichi chiaramente che si tratta di un metodo modello ... IMNSHO, le persone che scrivono applicazioni senza considerare l'API ( But.. I'm not a framework developer, I don't care about my API!) sono esattamente quelle persone che scrivono molte ( e anche codice non documentato e solitamente illeggibile), non viceversa.
Laoujin

44

Le interfacce marker vengono utilizzate per contrassegnare la capacità di una classe come implementazione di un'interfaccia specifica in fase di esecuzione.

Le linee guida per la progettazione dell'interfaccia e la progettazione dei tipi .NET - Progettazione dell'interfaccia scoraggiano l'uso di interfacce marker a favore dell'utilizzo di attributi in C #, ma come sottolinea @Jay Bazuzi, è più facile controllare le interfacce dei marker che gli attributi:o is I

Quindi, invece di questo:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

Le linee guida .NET consigliano di eseguire questa operazione:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 

26
Inoltre, possiamo usare completamente i generici con interfacce marker, ma non con attributi.
Jordão

18
Anche se amo gli attributi e il loro aspetto da un punto di vista dichiarativo, non sono cittadini di prima classe in fase di esecuzione e richiedono una quantità significativa di impianti idraulici di livello relativamente basso con cui lavorare.
Jesse C. Slicer

4
@ Jordão - Questo era esattamente il mio pensiero. Ad esempio, se voglio astrarre il codice di accesso al database (diciamo Linq a Sql), avere un'interfaccia comune lo rende MOLTO più semplice. In effetti, non penso che sarebbe possibile scrivere quel tipo di astrazione con attributi poiché non è possibile eseguire il cast su un attributo e non è possibile utilizzarli nei generici. Suppongo che potresti usare una classe base vuota da cui derivano tutte le altre classi, ma che sembra più o meno come avere un'interfaccia vuota. Inoltre, se in seguito ti rendi conto di aver bisogno di funzionalità condivise, il meccanismo è già in atto.
tandrewnichols

23

Poiché ogni altra risposta ha affermato "dovrebbero essere evitati", sarebbe utile avere una spiegazione del perché.

Innanzitutto, perché vengono utilizzate le interfacce marker: esistono per consentire al codice che utilizza l'oggetto che lo implementa di verificare se implementano detta interfaccia e, se lo fa, trattano l'oggetto in modo diverso.

Il problema con questo approccio è che rompe l'incapsulamento. L'oggetto stesso ora ha il controllo indiretto su come verrà utilizzato esternamente. Inoltre, è a conoscenza del sistema in cui verrà utilizzato. Applicando l'interfaccia del marker, la definizione della classe suggerisce che si aspetta di essere utilizzato da qualche parte che controlla l'esistenza del marker. Ha una conoscenza implicita dell'ambiente in cui viene utilizzato e sta cercando di definire come dovrebbe essere utilizzato. Questo va contro l'idea di incapsulamento perché ha la conoscenza dell'implementazione di una parte del sistema che esiste interamente al di fuori del proprio ambito.

A livello pratico ciò riduce la portabilità e la riutilizzabilità. Se la classe viene riutilizzata in un'applicazione diversa, anche l'interfaccia deve essere copiata e potrebbe non avere alcun significato nel nuovo ambiente, rendendola completamente ridondante.

In quanto tale, il "marker" sono i metadati della classe. Questi metadati non vengono utilizzati dalla classe stessa ed è significativo solo per (alcuni!) Codice client esterno in modo che possa trattare l'oggetto in un certo modo. Poiché ha significato solo per il codice client, i metadati dovrebbero essere nel codice client, non nell'API della classe.

La differenza tra una "interfaccia marker" e un'interfaccia normale è che un'interfaccia con metodi dice al mondo esterno come può essere usata mentre un'interfaccia vuota implica che sta dicendo al mondo esterno come dovrebbe essere usata.


1
Lo scopo principale di qualsiasi interfaccia è distinguere tra classi che promettono di rispettare il contratto associato a tale interfaccia e quelle che non lo fanno. Mentre un'interfaccia è anche responsabile della fornitura delle firme di chiamata di qualsiasi membro necessario per adempiere al contratto, è il contratto, piuttosto che i membri, che determina se una particolare interfaccia deve essere implementata da una particolare classe. Se il contratto per IConstructableFromString<T>specifica che una classe Tpuò essere implementata solo IConstructableFromString<T>se ha un membro statico ...
supercat

... public static T ProduceFromString(String params);, una classe complementare all'interfaccia può offrire un metodo public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; se il codice client avesse un metodo simile T[] MakeManyThings<T>() where T:IConstructableFromString<T>, si potrebbero definire nuovi tipi che potrebbero funzionare con il codice client senza dover modificare il codice client per gestirli. Se i metadati fossero nel codice client, non sarebbe possibile creare nuovi tipi per l'utilizzo da parte del client esistente.
supercat

Ma il contratto tra Te la classe che lo utilizza è IConstructableFromString<T>dove hai un metodo nell'interfaccia che descrive alcuni comportamenti, quindi non è un'interfaccia marker.
Tom B

Il metodo statico che la classe deve avere non fa parte dell'interfaccia. I membri statici nelle interfacce vengono implementati dalle interfacce stesse; non c'è modo per un'interfaccia di fare riferimento a un membro statico in una classe di implementazione.
supercat

È possibile che un metodo determini, utilizzando Reflection, se un tipo generico ha un particolare metodo statico ed esegua quel metodo se esiste, ma il processo effettivo di ricerca ed esecuzione del metodo statico ProduceFromStringnell'esempio sopra non coinvolgerebbe l'interfaccia in qualsiasi modo, tranne per il fatto che l'interfaccia sarebbe usata come un marker per indicare quali classi ci si dovrebbe aspettare per implementare la funzione necessaria.
supercat

8

Le interfacce dei marker a volte possono essere un male necessario quando una lingua non supporta tipi di unione discriminati .

Supponiamo di voler definire un metodo che si aspetta un argomento il cui tipo deve essere esattamente uno tra A, B o C.In molti linguaggi funzionali (come F # ), un tale tipo può essere chiaramente definito come:

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

Tuttavia, nei linguaggi OO-first come C #, ciò non è possibile. L'unico modo per ottenere qualcosa di simile qui è definire l'interfaccia IArg e "contrassegnare" A, B e C con essa.

Ovviamente, potresti evitare di utilizzare l'interfaccia marker semplicemente accettando il tipo "oggetto" come argomento, ma in tal caso perderai espressività e un certo grado di sicurezza dei tipi.

I tipi di unione discriminati sono estremamente utili e esistono nei linguaggi funzionali da almeno 30 anni. Stranamente, fino ad oggi, tutti i principali linguaggi OO hanno ignorato questa caratteristica, sebbene in realtà non abbia nulla a che fare con la programmazione funzionale di per sé, ma appartenga al sistema dei tipi.


Vale la pena notare che poiché a Foo<T>avrà un set separato di campi statici per ogni tipo T, non è difficile che una classe generica contenga campi statici contenenti delegati per elaborare a Te prepopola quei campi con funzioni per gestire ogni tipo della classe è dovrebbe funzionare con. L'uso di un vincolo di interfaccia generico sul tipo Tverificherebbe al momento del compilatore che il tipo fornito almeno affermasse di essere valido, anche se non sarebbe stato in grado di garantire che lo fosse effettivamente.
supercat

6

Un'interfaccia marker è solo un'interfaccia vuota. Una classe implementerebbe questa interfaccia come metadati da utilizzare per qualche motivo. In C # useresti più comunemente gli attributi per contrassegnare una classe per gli stessi motivi per cui useresti un'interfaccia marker in altri linguaggi.


4

Un'interfaccia marker consente di etichettare una classe in un modo che verrà applicato a tutte le classi discendenti. Un'interfaccia marker "pura" non definirebbe né erediterebbe nulla; un tipo più utile di interfacce marker può essere quella che "eredita" un'altra interfaccia ma non definisce nuovi membri. Ad esempio, se esiste un'interfaccia "IReadableFoo", si potrebbe anche definire un'interfaccia "IImmutableFoo", che si comporterebbe come un "Foo" ma prometterebbe a chiunque la utilizzi che nulla cambierebbe il suo valore. Una routine che accetta un IImmutableFoo sarebbe in grado di usarlo come farebbe un IReadableFoo, ma la routine accetterebbe solo le classi dichiarate come implementanti IImmutableFoo.

Non riesco a pensare a molti usi per interfacce marker "pure". L'unico a cui riesco a pensare sarebbe se EqualityComparer (di T) .Default restituisse Object.Equals per qualsiasi tipo che ha implementato IDoNotUseEqualityComparer, anche se il tipo implementava anche IEqualityComparer. Ciò consentirebbe di avere un tipo immutabile non sigillato senza violare il principio di sostituzione di Liskov: se il tipo sigilla tutti i metodi relativi al test di uguaglianza, un tipo derivato potrebbe aggiungere campi aggiuntivi e renderli mutabili, ma la mutazione di tali campi no ' essere visibile utilizzando qualsiasi metodo di tipo base. Potrebbe non essere orribile avere una classe immutabile non sigillata ed evitare qualsiasi utilizzo di EqualityComparer.Default o considerare classi derivate attendibili per non implementare IEqualityComparer,


4

Questi due metodi di estensione risolveranno la maggior parte dei problemi che Scott afferma favoriscono le interfacce dei marker rispetto agli attributi:

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

Adesso hai:

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

contro:

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

Non riesco a vedere come la creazione di un'API richiederà 5 volte il tempo con il primo pattern rispetto al secondo, come afferma Scott.


1
Ancora niente generici.
Ian Kemp

0

I marker sono interfacce vuote. Un marker o è lì o non lo è.

classe Foo: IConfidential

Qui contrassegniamo Foo come riservato. Non sono richieste proprietà o attributi aggiuntivi effettivi.


0

L'interfaccia Marker è un'interfaccia totalmente vuota che non ha corpo / dati membri / implementazione.
Una classe implementa l' interfaccia marker quando richiesto, serve solo per " contrassegnare "; significa che dice alla JVM che la particolare classe ha lo scopo di clonare, quindi consenti la clonazione. Questa particolare classe serve per serializzare i suoi oggetti, quindi consenti ai suoi oggetti di essere serializzati.


0

L'interfaccia del marker è in realtà solo una programmazione procedurale in un linguaggio OO. Un'interfaccia definisce un contratto tra implementatori e consumatori, tranne un'interfaccia marker, perché un'interfaccia marker non definisce altro che se stessa. Quindi, appena fuori dal cancello, l'interfaccia marker fallisce allo scopo fondamentale di essere un'interfaccia.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.