Che cosa intende l'autore colando il riferimento all'interfaccia per qualsiasi implementazione?

15

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

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

Developers who are new to the concept of programming to interfaces often have difficulty letting go of what is behind the interface.

At compile time, any client of an interface should have no idea which implementation of the interface it is using. Such knowledge can lead to incorrect assumptions that couple the client to a specific implementation of the interface.

Imagine the common example in which a class needs to save a record in persistent storage. To do so, it rightly delegates to an interface, which hides the details of the persistent storage mechanism used. However, it would not be right to make any assumptions about which implementation of the interface is being used at run time. For example, casting the interface reference to any implementation is always a bad idea.

Potrebbe essere la barriera linguistica o la mia mancanza di esperienza, ma non capisco cosa significhi. Ecco quello che capisco:

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

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 il seguente

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 sono entrato in tutti i riferimenti di classe e li ho sostituiti con ISomeClass.

Tranne che nella costruzione, dove ho scritto:

ISomeClass myClass = new SomeClass();

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

    
posta Marshall 11.09.2017 - 09:47
fonte

6 risposte

36

L'astrazione della classe in un'interfaccia è qualcosa che dovresti considerare se e solo se intendi scrivere altre implementazioni di detta interfaccia o se esiste la strong possibilità di farlo in futuro.

Quindi forse SomeClass e ISomeClass sono un cattivo esempio, perché sarebbe come avere una classe OracleObjectSerializer e un'interfaccia IOracleObjectSerializer .

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

Ovunque nel tuo programma dovresti usare IObjectSerializer senza preoccuparti di come funziona. Supponiamo per un secondo di avere anche un'implementazione SQLServerObjectSerializer oltre a OracleObjectSerializer . Ora supponiamo di dover impostare alcune proprietà speciali da impostare e tale metodo è presente solo in OracleObjectSerializer e non in SQLServerObjectSerializer.

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

Il modo sbagliato

Il modo errato e la stessa istanza a cui fa riferimento il tuo libro, sarebbe prendere un'istanza di IObjectSerializer e lanciarla su OracleObjectSerializer e quindi chiamare il metodo setProperty disponibile solo su OracleObjectSerializer . Questo è sbagliato perché anche se potresti conoscere un'istanza come OracleObjectSerializer , stai introducendo un altro punto nel tuo programma in cui ti interessa sapere quale implementazione è. Quando quell'implementazione cambia e presumibilmente prima o poi avrà più implementazioni, nel migliore dei casi, sarà necessario trovare tutti questi posti e apportare le correzioni corrette. Nel peggiore dei casi, lanci un'istanza IObjectSerializer a OracleObjectSerializer e ricevi un errore di runtime in produzione.

Principio di sostituzione di Liskov Approccio

Liskov ha detto che non dovresti mai aver bisogno di metodi come setProperty nella classe di implementazione come nel caso del mio OracleObjectSerializer se fatto correttamente. Se astraggo una classe OracleObjectSerializer in IObjectSerializer , dovresti racchiudere tutti i metodi necessari per usare quella classe, e se non puoi, allora c'è qualcosa di sbagliato nella tua astrazione (prova a fare una classe Dog come un'implementazione IPerson per esempio).

L'approccio corretto sarebbe quello di fornire un metodo setProperty a IObjectSerializer . Metodi simili in SQLServerObjectSerializer funzionano idealmente con questo metodo setProperty . Ancora meglio, standardizzate i nomi delle proprietà attraverso un Enum in cui ogni implementazione traduce quell'enum nell'equivalente per la sua terminologia di database.

In parole povere, l'utilizzo di ISomeClass è solo la metà di esso. Non dovresti mai aver bisogno di lanciarlo al di fuori del metodo che è responsabile della sua creazione. Fare ciò è quasi certamente un grave errore di progettazione.

    
risposta data 11.09.2017 - 10:08
fonte
27

La risposta accettata è corretta e molto utile, ma vorrei soffermarmi brevemente sulla riga di codice che hai chiesto riguardo a:

ISomeClass myClass = new SomeClass();

In generale, questo non è terribile. La cosa che dovrebbe essere evitata quando possibile farebbe questo:

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

Dove il codice viene fornito esternamente all'interfaccia, ma internamente lo converte in un'implementazione specifica, "Perché so che sarà solo l'implementazione". Anche se questo finiva per essere vero, usando un'interfaccia e trasmettendola a un'implementazione, stai volontariamente abbandonando la sicurezza del tipo reale solo così puoi fingere 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, suppone che qualsiasi implementazione di tale interfaccia sia un'opzione valida da passare. Potrebbe anche essere un po 'giù per te linea dopo aver dimenticato che un metodo specifico giace su quali parametri ha bisogno. Se senti la necessità di eseguire il cast da un'interfaccia a un'implementazione specifica, allora l'interfaccia, l'implementazione o il codice che li fa riferimento sono progettati in modo errato e dovrebbero cambiare. Ad esempio, se il metodo funziona solo quando l'argomento passato è una classe specifica, il parametro deve solo accettare quella classe.

Ora, tornando alla tua chiamata al costruttore

ISomeClass myClass = new SomeClass();

i problemi di casting non si applicano veramente. Nessuno di questi sembra essere esposto esternamente, quindi non vi è alcun rischio particolare associato ad esso. Essenzialmente, questa linea di codice è di per sé 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 quello che fanno. Tuttavia, anche questo non GUADAGNA nulla dall'esistenza di un'interfaccia. La tua myClass ha il tipo ISomeClass , ma non ha alcun motivo poiché è sempre assegnata un'implementazione specifica, SomeClass . Ci sono alcuni potenziali vantaggi minori, come la possibilità di scambiare l'implementazione nel codice cambiando solo la chiamata del costruttore, o riassegnando quella variabile in seguito a un'implementazione diversa, ma a meno che non ci sia qualcun altro che richiede che la variabile venga digitata nell'interfaccia anziché l'implementazione di questo modello rende il tuo codice simile alle interfacce utilizzate solo a memoria, non a causa della reale comprensione dei vantaggi delle interfacce.

    
risposta data 11.09.2017 - 15:25
fonte
2

Penso che sia più facile mostrare il tuo codice con un brutto 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 scrivi inizialmente il codice, probabilmente c'è solo un'implementazione di quell'interfaccia, così che il cast continuerà a funzionare, è solo che in futuro potresti implementare un'altra classe e poi (come mostra il mio esempio ), si tenta di accedere a dati che non esistono nell'oggetto che si sta utilizzando.

    
risposta data 11.09.2017 - 14:57
fonte
2

Solo per chiarezza, definiamo il casting.

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

Ecco un esempio di casting da questo documento Microsoft pagina .

// 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;  

potresti fare la stessa cosa e lanciare qualcosa che implementa un'interfaccia per un'implementazione particolare di quell'interfaccia, ma tu non dovresti perché ciò comporterà un errore o comportamento imprevisto se viene utilizzata un'implementazione diversa da quella prevista.

    
risposta data 11.09.2017 - 14:14
fonte
0

I miei 5 centesimi:

Tutti questi esempi sono Ok, ma non sono esempi reali e non hanno mostrato le intenzioni del mondo reale.

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

Supponi di avere un'interfaccia iList :

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

Ora supponiamo che ci siano molte implementazioni:

  • DynamicArrayList - utilizza un array piatto, veloce da inserire e rimuovere alla fine.
  • LinkedList - utilizza un doppio elenco collegato, veloce da inserire davanti e alla fine.
  • AVLTreeList - utilizza AVL Tree, veloce per fare tutto, ma usa molta memoria
  • SkipList - usa SkipList, veloce per fare tutto, più lento di AVL Tree, ma usa meno memoria.
  • HashList - utilizza HashTable

Si può pensare a molte diverse implementazioni.

Ora supponiamo di avere il seguente codice:

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

Mostra chiaramente la nostra intenzione che vogliamo utilizzare iList . Certo non siamo più in grado di eseguire DynamicArrayList operazioni specifiche, ma abbiamo bisogno di un iList .

Considera il seguente codice:

iList list = factory.getList();

Ora non sappiamo nemmeno quale sia l'implementazione. Quest'ultimo esempio è spesso usato nell'elaborazione delle immagini quando si carica un file dal disco e non è necessario il suo tipo di file (gif, jpeg, png, bmp ...), ma tutto ciò che si vuole è manipolare le immagini (capovolgere, scala, salva come png alla fine).

    
risposta data 11.09.2017 - 23:16
fonte
0

Hai un'interfaccia ISomeClass e un oggetto myObject di cui non conosci nulla dal tuo codice, tranne che è dichiarato che implementa ISomeClass.

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

Cosa c'è di sbagliato nel lanciare myClass su SomeClass? Due cose sono sbagliate. Uno, 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 ottieni 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, naturalmente, in molte altre classi che implementano ISomeClass. Se una chiamata finisce nel tuo codice SomeClass.myMethod, allora sai che self è un'istanza di SomeClass, ea quel punto è assolutamente corretto e in realtà corretto usarlo come oggetto SomeClass. Ovviamente se si tratta in realtà di un'istanza di OtherClass e non di SomeClass, allora non si arriverà al codice SomeClass.

    
risposta data 12.09.2017 - 08:33
fonte

Leggi altre domande sui tag