2012-07-11 28 views
6

Contesto: Nello spirito di "program to an interface, not an implementation" e Haskell type classes e come esperimento di codifica, sto pensando a cosa significherebbe creare un'API basata principalmente sulla combinazione di interfacce e metodi di estensione. Ho due linee guida in mente:"programmazione di un'interfaccia" utilizzando metodi di estensione: quando va a finire troppo lontano?

  1. Evitare l'ereditarietà della classe quando possibile. Interfacce dovrebbero essere implementati come sealed class es.
    (Questo per due ragioni:. Primo, perché sottoclassi solleva alcune questioni sgradevoli su come specificare e far rispettare il contratto di classe di base nelle sue classi derivate In secondo luogo, e questo è il tipo di Haskell classe di influenza, il polimorfismo non richiede la creazione di sottoclassi .)

  2. Evitare metodi di istanza, ove possibile. Se può essere fatto con i metodi di estensione, questi sono preferiti.
    (Questo è inteso per aiutare a mantenere le interfacce compatta:.. Tutto ciò che può essere fatto mediante una combinazione di altri metodi di istanza diventa un metodo di estensione Resta nell'interfaccia è funzionalità di base, e metodi che cambia stato in particolare)

Problema: Ho problemi con la seconda linea guida. Considerate questo:

interface IApple { } 
static void Eat(this IApple apple) 
{ 
    Console.WriteLine("Yummy, that was good!"); 
} 

interface IRottenApple : IApple { } 
static void Eat(this IRottenApple apple) 
{ 
    Console.WriteLine("Eat it yourself, you disgusting human, you!"); 
} 

sealed class RottenApple : IRottenApple { } 
IApple apple = new RottenApple(); 
// API user might expect virtual dispatch to happen (as usual) when 'Eat' is called: 
apple.Eat(); // ==> "Yummy, that was good!" 

Ovviamente, per il risultato atteso ("Eat it yourself…"), Eat dovrebbe essere un metodo di istanza regolare.

Domanda: Quale sarebbe una linea guida raffinata/più accurata sull'uso dei metodi di estensione rispetto ai metodi di istanza (virtuali)? Quando l'uso dei metodi di estensione per "programmare su un'interfaccia" è eccessivo? In quali casi sono effettivamente necessari i metodi di istanza?

Non so se lo è una regola generale chiara, quindi non mi aspetto una risposta universale perfetta. Sono apprezzati eventuali miglioramenti ben argomentati alla linea guida (2) sopra riportata.

+0

penso un posto migliore per iniziare sarebbe '' IEnumerable Rathe r di un esempio inventato. – ChaosPandion

+0

beh, hai * kindof * violato la regola # 1 facendo 'IRottenApple' estendere' IApple' ... (anche se qui si legge * ereditarietà * classe) –

+0

@John: Non credo. Credo che l'ereditarietà * dell'interfaccia * eviti molti dei problemi della sottoclasse. (Ad esempio: un metodo sottoposto a override deve essere chiamato dalla classe derivata o no? Questa domanda svanisce semplicemente quando non si accetta la sottoclasse.) – stakx

risposta

6

tua linea guida è abbastanza buono come lo è: si dice già "per quanto possibile". Quindi il compito è davvero quello di precisare il bit "dove possibile" in alcuni dettagli.

utilizzo questa semplice dicotomia: se lo scopo di aggiungere un metodo è quello di nascondere le differenze tra sottoclassi, utilizzare un metodo di estensione; se lo scopo è quello di evidenziare le differenze, utilizzare un metodo virtuale.

Il tuo metodo Eat è un esempio di un metodo che introduce una differenza tra sottoclassi: il processo di mangiare (o meno) una mela dipende dal tipo di mela che è. Pertanto, dovresti implementarlo come metodo di istanza.

Un esempio di un metodo che cerca di nascondere le differenze sarebbe ThrowAway:

public static void ThrowAway(this IApple apple) { 
    var theBin = RecycleBins.FindCompostBin(); 
    if (theBin != null) { 
     theBin.Accept(apple); 
     return; 
    } 
    apple.CutUp(); 
    RecycleBins.FindGarbage().Accept(apple); 
} 

Se il processo di buttare via una mela è lo stesso indipendentemente dal tipo della mela, l'operazione è un numero primo candidato per essere implementato in un metodo di estensione.

+0

+1. Per nascondere o evidenziare la distinzione. Nota a margine Penso che dovresti dire "usa un metodo di interfaccia". invece di "utilizzare un metodo virtuale". –

+0

+1 e :-D. Ma non è proprio lo scopo dell'astrazione attraverso il polimorfismo di rendere diverse implementazioni * esteriormente * apparire uguale? Cioè, la corretta astrazione * non * incoraggia * metodi di occultamento delle differenze come 'ThrowAway'? – stakx

+0

Per quanto riguarda il mio commento precedente: l'ultimo paragrafo della risposta fornisce una buona guida per distinguere i due casi. Ho menzionato _ "diverse implementazioni" _ nel commento sopra, mentre stai dando un esempio in cui l'implementazione sarebbe sempre la stessa. Grazie per l'ottima risposta. – stakx

1

Per me l'output previsto era corretto. Digitate il cast (probabilmente usando quello sbagliato) come variabile IApple.

Ad esempio:

IApple apple = new RottenApple(); 
apple.Eat(); // "Yummy, that was good!" 
IRottenApple apple2 = new RottenApple(); 
apple2.Eat(); // "Eat it yourself, you disgusting human, you!" 
var apple3 = new RottenApple(); 
apple.Eat(); // "Eat it yourself, you disgusting human, you!" 

Domanda: Quale potrebbe essere una raffinata/linee guida più precise circa l'uso di metodi di estensione vs metodi di istanza (virtuali)? Quando l'uso dei metodi di estensione per "programmare su un'interfaccia" va lontano? In quali casi sono effettivamente necessari i metodi di istanza?

solo la mia opinione personale, quando lo sviluppo di applicazioni:

Io uso metodi di istanza quando sto scrivendo qualcosa che può o qualcun altro potrebbe consumare. Questo perché è un requisito per ciò che il tipo è in realtà. Considerare un'interfaccia/classe FlyingObject con un metodo Fly(). Questo è un metodo fondamentale di base di un oggetto volante. Creare un metodo di estensione non ha molto senso.

Uso (molto) dei metodi di estensione, ma questi non sono mai un requisito per l'utilizzo della classe che estendono. Ad esempio, ho il metodo di estensione su int che crea un SqlParameter (inoltre è interno). Tuttavia non ha senso avere quel metodo come parte della classe base di int, in realtà non ha nulla a che fare con ciò che un int è o non fa. Il metodo di estensione è un modo visivamente piacevole di creare un metodo riutilizzabile che consuma una classe/struttura.

+0

Ciò che mi preoccupa del mio esempio è che il codice suggerisce che "apple.Eat" stamperà un reclamo, perché è ciò che accadrebbe nel (consueto) caso del dispatch virtuale; tuttavia, come si afferma correttamente, i metodi di estensione non funzionano in questo modo. - Supponiamo che mangiare una mela sia * non * un'operazione fondamentale del tipo 'IApple', e che' Eat' sia quindi giustamente implementato come metodo di estensione. Ma allora cosa c'è di sbagliato nella mia linea guida (2)? Perché mi ha portato in una situazione così inaspettata? Come dovrei adattarlo in modo tale da rilevare e prevenire questi * prima * che qualcuno ci si imbatta in esso? – stakx

+0

Cosa accadrebbe se Apple e RottenApple fossero insieme in un carrello ? Sarebbero entrambi deliziosi! –

+0

@stakx Penso che sia solo una comprensione fondamentale che i metodi di estensione sono in realtà solo metodi statici. Pertanto, quando il compilatore tenta di cercare una firma del metodo, individuerà quella che corrisponde al più vicino possibile al tipo. 'IRottenApple' è una corrispondenza più stretta di' IApple', quindi i metodi che usano 'IRottenApple' saranno usati su' IApple'. –

0

Ho notato che C metodi # di estensione può essere molto simile al C++ funzioni non-amico non membri nel seguente modo: Sia Scott Meyers e Herb Sutter affermazione che in C++, incapsulamento a volte è aumentata di non fare una funzione un membro della classe:

"Laddove possibile, preferisci scrivere le funzioni come non amico non amico."Summary of Herb Sutter's GotW #84

(Sutter giustifica questo approccio nel suo article about the Interface Principle.)

Nel 1991, Scott Meyers anche formulato un algoritmo per decidere se una funzione dovrebbe essere una funzione membro, una funzione amico, o un non-amico funzione non membro:

se (f deve essere virtuale)
    fanno f una funzione membro di C;
else if (f è operator>> o operator<<)
    fanno f una funzione terzo;
    se (f esigenze accesso ai membri non pubblici di C)
        fare f amico di C;
else if (f esigenze tipo conversioni sul suo più a sinistra argomento)
    fanno f un terzo funzione;
    se (f esigenze accesso ai membri non pubblici di C)
        fare f amico di C;
else if (f possono essere implementate tramite interfaccia pubblica C s')
    fanno f un terzo funzione;
altro
    fanno f una funzione membro di C;

Scott Meyers expanded algorithm from 1998 (riformattato)

Alcuni di essi è ovviamente specifico per C++, ma dovrebbe essere abbastanza facile trovare un algoritmo analogo per il linguaggio C#. (Per cominciare, friend può essere approssimata in C# con internal accesso modificatore;. "Funzioni non-membri" possono essere sia i metodi di estensione o altri metodi statici)

cosa che l'algoritmo non dice è quando, o perché, f "deve essere virtuale".@dasblinkenlight's answer si spinge abbastanza in là nella spiegazione di quel pezzo.

domande

rilevanti su Stack Overflow:

Problemi correlati