2012-04-25 16 views
24

Sono stato colpevole di avere una relazione 1 a 1 tra le mie interfacce e le classi concrete quando si utilizza l'iniezione di dipendenza. Quando ho bisogno di aggiungere un metodo a un'interfaccia, finisco per rompere tutte le classi che implementano l'interfaccia.Iniezione di dipendenza con interfacce o classi

Questo è un semplice esempio, ma supponiamo di dover iniettare uno ILogger in una delle mie classi.

public interface ILogger 
{ 
    void Info(string message); 
} 

public class Logger : ILogger 
{ 
    public void Info(string message) { } 
} 

Avere una relazione 1-a-1 come questo sembra un odore di codice. Dal momento che ho una sola implementazione, ci sono potenziali problemi se creo una classe e contrassegno il metodo Info come virtuale per sovrascrivere i miei test invece di dover creare un'interfaccia solo per una singola classe?

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     // Log to file 
    } 
} 

Se mi serviva un'altra implementazione, posso sovrascrivere il metodo Info:

public class SqlLogger : Logger 
{ 
    public override void Info(string message) 
    { 
     // Log to SQL 
    } 
} 

Se ciascuna di queste classi hanno proprietà o metodi specifici che potrebbero creare un'astrazione che perde, ho potuto estrarre su una base classe:

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     throw new NotImplementedException(); 
    } 
} 

public class SqlLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

public class FileLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

il motivo per cui non ho contrassegnare la classe base come astratta perché se ho sempre voluto aggiungere un altro metodo, non mi rompere exis implementazioni Ad esempio, se il mio FileLogger necessitava di un metodo Debug, è possibile aggiornare la classe base Logger senza interrompere lo SqlLogger esistente.

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     throw new NotImplementedException(); 
    } 

    public virtual void Debug(string message) 
    { 
     throw new NotImplementedException(); 
    } 
} 

public class SqlLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

public class FileLogger : Logger 
{ 
    public override void Info(string message) { } 
    public override void Debug(string message) { } 
} 

Ancora, questo è un semplice esempio, ma quando dovrei preferire un'interfaccia?

+1

_Il motivo per cui non ho contrassegnato la classe base come astratto è perché se mai avessi voluto aggiungere un altro metodo_ Hm, una classe astratta può contenere un'implementazione. Puoi aggiungere il metodo Debug alla tua classe Logger astratta. –

+0

Rompere le implementazioni esistenti è solo un problema se si sta scrivendo una libreria riutilizzabile. Tu sei? O stai semplicemente scrivendo una linea di applicazione aziendale? – Steven

+0

Questa non è la portata della domanda, ma l'ereditarietà è sopravvalutata. Un 'SqlLogger' è solo un' Logger' concreto con una 'SqlLogPersistenceStrategy'. La composizione è molto meglio dell'eredità nella maggior parte dei casi. Anche per il tuo problema, per quanto riguarda l'ISP? 'ILogInfo',' ILogError', ecc. – plalx

risposta

27

La risposta "rapida"

vorrei bastone con le interfacce. Sono progettati come come contratti per il consumo di entità esterne.

@JakubKonecki ha menzionato l'ereditarietà multipla. Penso che questa sia la ragione principale per attaccare con le interfacce dato che diventerà molto evidente dal lato dei consumatori se li costringete a prendere una classe base ... a nessuno piacciono le classi base che vengono spinte su di loro.

La risposta Aggiornato "Quick"

Hai dichiarato problemi con implementazioni di interfacce di fuori del vostro controllo. Un buon approccio è semplicemente creare una nuova interfaccia ereditata da quella vecchia e correggere la propria implementazione.È quindi possibile notificare agli altri team che è disponibile una nuova interfaccia. Nel tempo, è possibile deprecare le interfacce più vecchie.

Non dimenticare che è possibile utilizzare il supporto di explicit interface implementations per mantenere una buona divisione tra interfacce che sono logicamente uguali, ma di versioni diverse.

Se si desidera che tutto ciò funzioni con DI, provare a non definire nuove interfacce e preferire invece le aggiunte. In alternativa, per limitare le modifiche al codice del client, prova ad ereditare nuove interfacce da quelle vecchie.

Attuazione vs Consumo

C'è una differenza tra attuazione l'interfaccia e consumare esso. L'aggiunta di un metodo interrompe l'implementazione (o le implementazioni), ma non infrange il consumatore.

La rimozione di un metodo rompe ovviamente il consumatore, ma non interrompe l'implementazione, tuttavia non lo si farebbe se si è consapevoli della retrocompatibilità per i propri utenti.

mie esperienze

Ci hanno spesso un rapporto 1-a-1 con le interfacce. È in gran parte una formalità, ma di tanto in tanto si ottengono delle buone istanze in cui le interfacce sono utili perché eseguiamo il test delle implementazioni di test o, in realtà, forniamo implementazioni specifiche per il cliente. Il fatto che questo spesso rompa quella implementazione se cambiamo l'interfaccia non è un odore di codice, a mio avviso, è semplicemente come si lavora contro le interfacce.

Il nostro approccio basato sull'interfaccia ci sta ora consolidando mentre utilizziamo tecniche come il modello di fabbrica e gli elementi di DI per migliorare una base di codice legacy obsoleta. I test sono stati in grado di trarre rapidamente vantaggio dal fatto che le interfacce esistevano nella base di codice per molti anni prima di trovare un uso "definitivo" (cioè, non solo 1-1 mapping con classi concrete).

Classe Base Contro classi

base sono per la condivisione di dettagli di implementazione per le entità comuni, il fatto che sono in grado di fare qualcosa di simile con la condivisione di un'API pubblica è un sottoprodotto, a mio parere. Le interfacce sono progettate per condividere le API pubblicamente, quindi usali.

Con le classi di base si potrebbe anche ottenere potenzialmente una perdita di dettagli di implementazione, ad esempio se è necessario rendere pubblico qualcosa da utilizzare per un'altra parte dell'implementazione. Questi non sono favorevoli al mantenimento di un'API pubblica pulita.

Rottura/supportare implementazioni

Se si va verso il basso il percorso interfaccia che si può incorrere in difficoltà cambiando anche l'interfaccia a causa di contratti di rottura. Inoltre, come dici, puoi interrompere le implementazioni al di fuori del tuo controllo. Esistono due modi per risolvere questo problema:

  1. Dichiarare che non si romperanno i consumatori, ma non si supporteranno le implementazioni.
  2. Dichiara che, una volta pubblicata un'interfaccia, non viene mai modificata.

ho assistito quest'ultimo, lo vedo sono disponibili in due forme:

  1. interfacce completamente separate per ogni novità: MyInterfaceV1, MyInterfaceV2.
  2. Ereditarietà interfaccia: MyInterfaceV2 : MyInterfaceV1.

io personalmente non sceglierei a proseguire su questa strada, io sceglierei di non supportare le implementazioni dalle variazioni di rottura. Ma a volte non abbiamo questa scelta.

Alcuni Codice

public interface IGetNames 
{ 
    List<string> GetNames(); 
} 

// One option is to redefine the entire interface and use 
// explicit interface implementations in your concrete classes. 
public interface IGetMoreNames 
{ 
    List<string> GetNames(); 
    List<string> GetMoreNames(); 
} 

// Another option is to inherit. 
public interface IGetMoreNames : IGetNames 
{ 
    List<string> GetMoreNames(); 
} 

// A final option is to only define new stuff. 
public interface IGetMoreNames 
{ 
    List<string> GetMoreNames(); 
} 
+0

Una delle difficoltà che ho è quando questa interfaccia è condivisa in tutta l'azienda. Se aggiorno un'interfaccia, interrompe le implementazioni per tutti gli altri team che utilizzano questa interfaccia. A questo punto, stiamo forzando questi cambiamenti su di loro. In un mondo perfetto, tutti sarebbero felici e disposti ad aggiornare le loro implementazioni, ma non è sempre così. – nivlam

+0

Se non è possibile modificare l'interfaccia, è possibile crearne un'altra: 'inteface IDebuger {void Debug (string message);}' e implementarlo in FileLogger. Quindi, se altri team non ne hanno bisogno, non lo useranno e non lo implementeranno. –

+0

@nivlam L'alternativa a questo è una volta che viene creata un'interfaccia, che è bloccata per cambiare. Crea nuove interfacce che ereditano quelle vecchie, quindi le implementazioni possono anche implementare le nuove interfacce ... cioè solo la tua implementazione interna. Ho aggiornato la mia risposta per soddisfare. –

2

Si dovrebbe sempre preferire l'interfaccia.

Sì, in alcuni casi si avranno gli stessi metodi su classe e interfaccia, ma in scenari più complessi non lo si farà. Inoltre, ricorda che non esiste un'eredità multipla in .NET.

È necessario mantenere le interfacce in un assembly separato e le classi dovrebbero essere interne.

Un altro vantaggio della codifica rispetto alle interfacce è la possibilità di prenderli facilmente in test di unità.

+0

Perché è consigliabile mantenere le interfacce in un assieme separato? – thedev

+4

@thedev È possibile pubblicare le interfacce senza pubblicare un'implementazione.Questi possono anche essere condivisi senza perdite di un'implementazione indesiderata che aggiunge solo problemi di manutenzione o di sovraccarico. –

0

preferisco interfacce. Dato che stub e mock sono anche implementazioni (sorta di), ho sempre almeno due implementazioni di qualsiasi interfaccia. Inoltre, le interfacce possono essere stubbate e derise per i test.

Inoltre, l'angolo del contratto menzionato da Adam Houldsworth è molto costruttivo. IMHO rende il codice più pulito di implementazioni di interfacce 1-1 lo rendono puzzolente.

9

L'interfaccia ILogger è rompere il interface segregation principle quando si inizia ad aggiungere Debug, Error, e Critical metodi oltre Info. Dai uno sguardo allo horrible Log4Net ILog interface e saprai di cosa sto parlando.

Invece di creare un metodo per la gravità di registro, creare un unico metodo che accetta un oggetto di registro:

void Log(LogEntry entry); 

Questo risolve completamente tutti i problemi, perché:

  1. LogEntry sarà un semplice DTO e puoi aggiungere nuove proprietà, senza rompere alcun client.
  2. È possibile creare una serie di metodi di estensione per l'interfaccia associata a quel singolo metodo Log.

Ecco un esempio di tale metodo di estensione:

public static class LoggerExtensions 
{ 
    public static void Debug(this ILogger logger, string message) 
    { 
     logger.Log(new LogEntry(message) 
     { 
      Severity = LoggingSeverity.Debug, 
     }); 
    } 

    public static void Info(this ILogger logger, string message) 
    { 
     logger.Log(new LogEntry(message) 
     { 
      Severity = LoggingSeverity.Information, 
     }); 
    } 
} 

Per una discussione più dettagliata su questo disegno, si prega di leggere this.

+0

Il problema ora è che il contratto non definisce quale livello di gravità è o non è supportato. Con l'ISP in mente, che dire di 'ILogInfo',' ILogError', 'ILogDebug', etc? – plalx

Problemi correlati