Cosa intende l'autore trasmettendo il riferimento dell'interfaccia a qualsiasi implementazione?


17

Attualmente sto cercando di padroneggiare C #, quindi sto leggendo Adaptive Code via C # di Gary McLean Hall .

Scrive di schemi e anti-schemi. Nella parte relativa alle implementazioni e alle interfacce scrive quanto segue:

Gli sviluppatori che sono nuovi al concetto di programmazione per le interfacce hanno spesso difficoltà a lasciar andare ciò che c'è dietro l'interfaccia.

Al momento della compilazione, qualsiasi client di un'interfaccia non dovrebbe avere idea dell'implementazione dell'interfaccia che sta utilizzando. Tale conoscenza può portare a presupposti errati che accoppiano il client a un'implementazione specifica dell'interfaccia.

Immagina l'esempio comune in cui una classe deve salvare un record in memoria persistente. Per fare ciò, giustamente delega a un'interfaccia, che nasconde i dettagli del meccanismo di archiviazione persistente utilizzato. Tuttavia, non sarebbe corretto formulare ipotesi su quale implementazione dell'interfaccia venga utilizzata in fase di esecuzione. Ad esempio, trasmettere il riferimento dell'interfaccia a qualsiasi implementazione è sempre una cattiva idea.

Potrebbe essere la barriera linguistica o la mia mancanza di esperienza, ma non capisco bene cosa significhi. Ecco cosa ho capito:

Ho un progetto divertente per il tempo libero per praticare C #. Lì ho una lezione:

public class SomeClass...

Questa classe è usata in molti posti. Durante l'apprendimento di C #, ho letto che è meglio astrarre con un'interfaccia, quindi ho fatto quanto segue

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Quindi ho inserito tutti i riferimenti di classe e li ho sostituiti con ISomeClass.

Tranne nella costruzione, dove ho scritto:

ISomeClass myClass = new SomeClass();

Sto capendo correttamente che questo è sbagliato? Se sì, perché sì, e cosa dovrei fare invece?


25
In nessun punto del tuo esempio stai lanciando un oggetto di tipo interfaccia sul tipo di implementazione. Stai assegnando qualcosa di tipo di implementazione a una variabile di interfaccia, che è perfettamente corretta e corretta.
Caleth,

1
Che cosa intendi con "costruttore" in cui ho scritto ISomeClass myClass = new SomeClass();? Se intendi davvero quello, è ricorsione nel costruttore, probabilmente non quello che vuoi. Speriamo che intendi per "costruzione", cioè allocazione, forse, ma non nel costruttore stesso, giusto ?
Erik Eidt,

@Erik: Sì. In costruzione. Hai ragione. Correggerà la domanda. Grazie
Marshall l'

Fatto curioso: F # ha una storia migliore di C # a questo proposito - elimina le implementazioni di interfaccia implicite, quindi ogni volta che vuoi chiamare un metodo di interfaccia, devi eseguire l'upgrade al tipo di interfaccia. Questo rende molto chiaro quando e come stai usando le interfacce nel tuo codice e rende la programmazione di interfacce molto più radicata nel linguaggio.
scrwtp,

3
Questo è un po 'fuori tema, ma penso che l'autore abbia diagnosticato erroneamente il problema che le persone nuove al concetto hanno. A mio avviso, il problema è che le persone nuove al concetto non sanno come realizzare buone interfacce. È molto facile creare interfacce troppo specifiche che in realtà non forniscono alcuna generalità (cosa che potrebbe accadere ISomeClass), ma è anche facile creare interfacce troppo generali per le quali è impossibile scrivere codice utile a quel punto le uniche opzioni devono ripensare l'interfaccia e riscrivere il codice o effettuare il downcast.
Derek Elkins lasciò l'

Risposte:


37

Astrarre la tua classe in un'interfaccia è qualcosa che dovresti considerare se e solo se intendi scrivere altre implementazioni di detta interfaccia o esiste la forte possibilità di farlo in futuro.

Quindi forse SomeClassed ISomeClassè un cattivo esempio, perché sarebbe come avere una OracleObjectSerializerclasse e IOracleObjectSerializerun'interfaccia.

Un esempio più accurato potrebbe essere qualcosa di simile OracleObjectSerializera IObjectSerializer. L'unico posto nel tuo programma in cui tieni a quale implementazione usare è quando viene creata l'istanza. A volte questo viene ulteriormente disaccoppiato usando un modello di fabbrica.

In qualsiasi altra parte del programma non dovresti IObjectSerializerpreoccuparti di come funziona. Supponiamo per un secondo ora che hai anche SQLServerObjectSerializerun'implementazione in aggiunta a OracleObjectSerializer. Supponiamo ora che sia necessario impostare alcune proprietà speciali da impostare e tale metodo sia presente solo in OracleObjectSerializer e non in SQLServerObjectSerializer.

Ci sono due modi per farlo: il modo sbagliato e l' approccio del principio di sostituzione di Liskov .

Il modo sbagliato

Il modo errato, e l'istanza a cui si fa riferimento nel tuo libro, sarebbe quello di prendere un'istanza IObjectSerializere lanciarla OracleObjectSerializere quindi chiamare il metodo setPropertydisponibile solo su OracleObjectSerializer. Questo è negativo perché anche se potresti sapere che un'istanza è una OracleObjectSerializer, stai introducendo un altro punto nel tuo programma in cui ti interessa sapere quale sia l'implementazione. Quando tale implementazione cambia, e presumibilmente lo sarà prima o poi se si hanno implementazioni multiple, nel migliore dei casi, sarà necessario trovare tutti questi luoghi e apportare le modifiche corrette. Nel peggiore dei casi, si trasmette IObjectSerializerun'istanza a OracleObjectSerializere si riceve un errore di runtime in produzione.

Approccio al principio di sostituzione di Liskov

Liskov ha detto che non dovresti mai aver bisogno di metodi come setPropertynella classe di implementazione come nel caso del mio OracleObjectSerializerse fatto correttamente. Se un classe astratta OracleObjectSerializerper IObjectSerializer, si dovrebbe comprendere tutti i metodi necessari per utilizzare quella classe, e se non è possibile, allora c'è qualcosa che non va con il vostro astrazione (cercando di fare un Doglavoro di classe come IPersonimplementazione per esempio).

L'approccio corretto sarebbe quello di fornire un setPropertymetodo a IObjectSerializer. Metodi simili SQLServerObjectSerializerfunzionerebbero idealmente con questo setPropertymetodo. Meglio ancora, standardizzi i nomi delle proprietà attraverso un punto in Enumcui ogni implementazione traduce quell'enum nell'equivalente per la propria terminologia del database.

In parole povere, usare an ISomeClassè solo la metà. Non dovresti mai aver bisogno di lanciarlo al di fuori del metodo che è responsabile della sua creazione. Farlo è quasi certamente un grave errore di progettazione.


1
Mi sembra che, se avete intenzione di lanciare IObjectSerializera OracleObjectSerializerperché “sapere” che questo è quello che è, allora si dovrebbe essere onesti con se stessi (e, soprattutto, con altri che possono mantenere tale codice, che possono includere il vostro futuro auto) e utilizzare OracleObjectSerializerfino in fondo da dove viene creato a dove viene utilizzato. Questo rende molto pubblico e chiaro che stai introducendo una dipendenza da una particolare implementazione, e il lavoro e la bruttezza coinvolti nel fare ciò diventano, di per sé, un forte suggerimento che qualcosa non va.
KRyan,

(E, se per qualche motivo devi davvero fare affidamento su una particolare implementazione, diventa molto più chiaro che questo è ciò che stai facendo e che lo stai facendo con intento e scopo. Questo "non dovrebbe" accadere ovviamente, e il 99% delle volte potrebbe sembrare che stia accadendo, in realtà non lo è e dovresti sistemare le cose, ma nulla è mai sicuro al 100% o come dovrebbero essere le cose.)
KRyan

@KRyan Assolutamente. L'astrazione dovrebbe essere usata solo se ne hai bisogno. L'uso dell'astrazione quando non è necessario serve solo a rendere il codice leggermente più difficile da capire.
Neil,

29

La risposta accettata è corretta e molto utile, ma vorrei indirizzare brevemente nello specifico la riga di codice che hai chiesto:

ISomeClass myClass = new SomeClass();

A grandi linee, questo non è terribile. La cosa che dovrebbe essere evitata quando possibile sarebbe fare questo:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Laddove al tuo codice viene fornita un'interfaccia esternamente, ma internamente la trasmette a un'implementazione specifica, "Perché so che sarà solo l'implementazione". Anche se ciò si è rivelato vero, usando un'interfaccia e lanciandola su un'implementazione, si rinuncia volontariamente alla sicurezza del tipo reale solo per far finta di usare l'astrazione. Se qualcun altro dovesse lavorare sul codice in un secondo momento e vedere un metodo che accetta un parametro di interfaccia, supporrà che qualsiasi implementazione di tale interfaccia sia un'opzione valida per passare. Potrebbe anche essere te stesso un po 'giù linea dopo aver dimenticato che un metodo specifico si trova su quali parametri ha bisogno. Se senti la necessità di trasmettere da un'interfaccia a un'implementazione specifica, allora l'interfaccia, l'implementazione, o il codice che li fa riferimento non è progettato correttamente e dovrebbe cambiare. Ad esempio, se il metodo funziona solo quando l'argomento passato è una classe specifica, il parametro dovrebbe accettare solo quella classe.

Ora, guardando indietro alla tua chiamata del costruttore

ISomeClass myClass = new SomeClass();

i problemi del casting non si applicano davvero. Nulla di tutto ciò sembra essere esposto esternamente, quindi non vi è alcun rischio particolare associato ad esso. In sostanza, questa stessa riga di codice è un dettaglio di implementazione che le interfacce sono progettate per astrarre per cominciare, quindi un osservatore esterno lo vedrebbe funzionare allo stesso modo indipendentemente da ciò che fanno. Tuttavia, anche questo non guadagna nulla dall'esistenza di un'interfaccia. Il tuo myClassha il tipo ISomeClass, ma non ha alcun motivo poiché gli viene sempre assegnata un'implementazione specifica,SomeClass. Ci sono alcuni potenziali vantaggi minori, come la possibilità di scambiare l'implementazione nel codice modificando solo la chiamata del costruttore o riassegnando quella variabile in seguito a un'implementazione diversa, ma a meno che non ci sia un altro posto che richieda che la variabile sia digitata sull'interfaccia anziché l'implementazione di questo modello fa apparire il tuo codice come se le interfacce fossero utilizzate solo da rote, non per una reale comprensione dei vantaggi delle interfacce.


1
Apache Math lo fa con Cartesian3D Source, linea 286 , che può essere davvero fastidioso.
J_F_B_M,

1
Questa è la risposta effettivamente corretta che affronta la domanda originale.
Benjamin Gruenbaum,

2

Penso che sia più facile mostrare il tuo codice con un cattivo esempio:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

Il problema è che quando inizialmente scrivi il codice, probabilmente c'è solo un'implementazione di quell'interfaccia, quindi il casting funzionerebbe ancora, è solo che in futuro, potresti implementare un'altra classe e poi (come mostra il mio esempio), tu prova ad accedere ai dati che non esistono nell'oggetto che stai utilizzando.


Perché ciao signore. Qualcuno ti ha mai detto quanto sei bello?
Neil,

2

Per chiarezza, definiamo il casting.

Il casting sta convertendo forzatamente qualcosa da un tipo a un altro. Un esempio comune è il cast di un numero in virgola mobile in un tipo intero. È possibile specificare una conversione specifica durante il cast, ma l'impostazione predefinita è semplicemente reinterpretare i bit.

Ecco un esempio di casting da questa pagina di documenti Microsoft .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

Si potrebbe fare la stessa cosa e cast qualcosa che implementa un'interfaccia ad una particolare implementazione di tale interfaccia, ma si dovrebbe non perché questo si tradurrà in un errore o un comportamento imprevisto se si utilizza una diversa implementazione del previsto.


1
"Il casting sta convertendo qualcosa da un tipo a un altro." - No. Il casting sta convertendo esplicitamente qualcosa da un tipo a un altro. (In particolare, "cast" è il nome della sintassi utilizzata per specificare quella conversione.) Le conversioni implicite non sono cast. "È possibile specificare una conversione specifica durante il cast, ma l'impostazione predefinita è semplicemente reinterpretare i bit." -- Certamente no. Esistono molte conversioni, sia implicite che esplicite, che comportano modifiche significative ai modelli di bit.
hvd,

@hvd Ho fatto una correzione ora per quanto riguarda l'esplicitazione del casting. Quando ho detto che il default è semplicemente reinterpretare i bit, stavo cercando di esprimere che se dovessi creare il tuo tipo, quindi nei casi in cui un cast viene automaticamente definito, quando lo cast su un altro tipo i bit verranno reinterpretati . Nell'esempio Animal/ Giraffesopra, se si dovessero fare Animal a = (Animal)g;i bit verrebbero reinterpretati (qualsiasi dato specifico di Giraffe verrebbe interpretato come "non parte di questo oggetto").
Ryan1729,

Nonostante ciò che dice hvd, la gente usa molto spesso il termine "cast" in riferimento a conversioni implicite; vedi ad esempio https://www.google.com/search?q="implicit+cast"&tbm=bks . Tecnicamente penso che sia più corretto riservare il termine "cast" per conversioni esplicite, purché non ti confonda quando altri lo usano in modo diverso.
Ruakh,

0

I miei 5 centesimi:

Tutti quegli esempi sono ok, ma non sono esempi del mondo reale e non hanno mostrato le intenzioni del mondo reale.

Non conosco C # quindi darò un esempio astratto (mix tra Java e C ++). Spero che vada bene.

Supponiamo di avere un'interfaccia iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Supponiamo ora che ci siano molte implementazioni:

  • DynamicArrayList: utilizza una matrice piatta, veloce da inserire e rimuovere alla fine.
  • LinkedList: utilizza un doppio elenco di collegamenti, rapido da inserire davanti e alla fine.
  • AVLTreeList: utilizza AVL Tree, veloce per fare tutto, ma utilizza molta memoria
  • SkipList: utilizza SkipList, veloce per fare tutto, più lentamente di AVL Tree, ma utilizza meno memoria.
  • HashList: utilizza HashTable

Si può pensare a molte implementazioni diverse.

Supponiamo ora di avere il seguente codice:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Mostra chiaramente la nostra intenzione che vogliamo usare iList. Sicuramente non saremo più in grado di eseguire DynamicArrayListoperazioni specifiche, ma abbiamo bisogno di un iList.

Prendi in considerazione il seguente codice:

iList list = factory.getList();

Ora non sappiamo nemmeno quale sia l'implementazione. Quest'ultimo esempio viene spesso utilizzato nell'elaborazione delle immagini quando si carica un file dal disco e non è necessario il tipo di file (gif, jpeg, png, bmp ...), ma tutto ciò che si desidera è fare una manipolazione dell'immagine (capovolgere, scala, salva come png alla fine).


0

Hai un'interfaccia ISomeClass e un oggetto myObject di cui non sai nulla dal tuo codice tranne che è dichiarato per implementare ISomeClass.

Hai una classe SomeClass, che conosci implementa l'interfaccia ISomeClass. Lo sai perché è stato dichiarato per implementare ISomeClass o lo hai implementato tu stesso per implementare ISomeClass.

Cosa c'è di sbagliato nel trasmettere myClass a SomeClass? Due cose sono sbagliate. Primo, non sai davvero che myClass è qualcosa che può essere convertito in SomeClass (un'istanza di SomeClass o una sottoclasse di SomeClass), quindi il cast potrebbe andare storto. Due, non dovresti farlo. Dovresti lavorare con myClass dichiarato come iSomeClass e utilizzare i metodi ISomeClass.

Il punto in cui si ottiene un oggetto SomeClass è quando viene chiamato un metodo di interfaccia. Ad un certo punto chiami myClass.myMethod (), che è dichiarato nell'interfaccia, ma ha un'implementazione in SomeClass, e ovviamente in molte altre classi che implementano ISomeClass. Se una chiamata finisce nel tuo codice SomeClass.myMethod, allora sai che self è un'istanza di SomeClass, e a quel punto è assolutamente corretto e effettivamente corretto usarlo come oggetto SomeClass. Naturalmente se si tratta in realtà di un'istanza di OtherClass e non di SomeClass, non si arriva al codice SomeClass.

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.