Qual è lo scopo di un'interfaccia marker?
Qual è lo scopo di un'interfaccia marker?
Risposte:
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:
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:
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:
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 è:
virtual protected
metodo modello DoSomethingCore
invece di DoSomething
non è 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.
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
{
...
}
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.
IConstructableFromString<T>
specifica che una classe T
può essere implementata solo IConstructableFromString<T>
se ha un membro statico ...
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.
T
e la classe che lo utilizza è IConstructableFromString<T>
dove hai un metodo nell'interfaccia che descrive alcuni comportamenti, quindi non è un'interfaccia marker.
ProduceFromString
nell'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.
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.
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 T
e prepopola quei campi con funzioni per gestire ogni tipo della classe è dovrebbe funzionare con. L'uso di un vincolo di interfaccia generico sul tipo T
verificherebbe 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.
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.
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,
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.
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.
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.
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.