Dichiarare l'interfaccia nello stesso file della classe base, è una buona pratica?


29

Per essere intercambiabili e verificabili, normalmente i servizi con logica devono avere un'interfaccia, ad es

public class FooService: IFooService 
{ ... }

Dal punto di vista del design, sono d'accordo con questo, ma una delle cose che mi disturba con questo approccio è che per un servizio dovrai dichiarare due cose (la classe e l'interfaccia), e nel nostro team, normalmente due file (uno per la classe e uno per l'interfaccia). Un altro disagio è la difficoltà di navigazione perché l'uso di "Vai alla definizione" in IDE (VS2010) punterà all'interfaccia (poiché altre classi si riferiscono all'interfaccia), non alla classe effettiva.

Stavo pensando che scrivere IFooService nello stesso file di FooService ridurrà la stranezza di cui sopra. Dopotutto, IFooService e FooService sono molto correlati. È una buona pratica? C'è una buona ragione per cui IFooService deve trovarsi nel suo file?


3
Se hai solo un'implementazione di una particolare interfaccia, non c'è alcuna reale necessità logica per l'interfaccia. Perché non usare direttamente la classe?
Joris Timmermans,

11
@MadKeithV per accoppiamento lento e testabilità?
Louis Rhys,

10
@MadKeithV: hai ragione. Ma quando si scrivono unit test per il codice che si basa su IFooService, in genere si fornisce un MockFooService, che è una seconda implementazione di tale interfaccia.
Doc Brown,

5
@DocBrown - se hai più implementazioni, ovviamente il commento scompare, ma anche l'utilità di poter andare direttamente alla "singola implementazione" (perché ce ne sono almeno due). Quindi la domanda diventa: quali informazioni sono nell'implementazione concreta che non dovrebbero già essere nella descrizione / documentazione dell'interfaccia nel punto in cui si sta utilizzando l'interfaccia?
Joris Timmermans,

Risposte:


24

Non deve essere nel suo file, ma il tuo team dovrebbe decidere su uno standard e attenersi ad esso.

Inoltre, hai ragione che "Vai alla definizione" ti porta all'interfaccia, ma se hai installato Resharper , è solo un clic per aprire un elenco di classi / interfacce derivate da tale interfaccia, quindi non è un grosso problema. Ecco perché mantengo l'interfaccia in un file separato.

 


5
Sono d'accordo su questa risposta, dopo tutto l'IDE dovrebbe adattarsi al nostro design e non viceversa, giusto?
Marktani,

1
Inoltre, senza Resharper, F12 e Shift + F12 fanno praticamente la stessa cosa. Nemmeno un clic del mouse :) (per essere onesti, ci vorranno solo usi diretti dell'interfaccia, non sono sicuro di cosa faccia la versione di Resharper)
Daniel B

@DanielB: in Resharper, Alt-End passa all'implementazione (o ti presenta un elenco di possibili implementazioni a cui andare).
StriplingWarrior l'

@StriplingWarrior sembra che sia esattamente quello che fa anche Vanilla VS. F12 = vai alla definizione, Shift + F12 = vai all'implementazione (sono abbastanza sicuro che funzioni senza Resharper, anche se l'ho installato, quindi è difficile confermare). È una delle scorciatoie di navigazione più utili, sto solo spargendo la voce.
Daniel B,

@DanielB: L'impostazione predefinita per Shift-F12 è "trova utilizzo". jetbrains.com/resharper/webhelp/…
StriplingWarrior il

15

Penso che dovresti tenerli file separati. Come hai detto, l'idea è di rimanere testabile e intercambiabile. Posizionando l'interfaccia nello stesso file dell'implementazione, si associa l'interfaccia con l'implementazione specifica. Se si decide di creare un oggetto simulato o un'altra implementazione, l'interfaccia non sarà logicamente separata da FooService.


10

Secondo SOLID, non solo dovresti creare l'interfaccia e non solo dovrebbe essere in un file diverso, ma dovrebbe essere in un assembly diverso.

Perché? Poiché qualsiasi modifica a un file di origine che viene compilata in un assembly richiede la ricompilazione dell'assembly e qualsiasi modifica a un assembly richiede la ricompilazione di qualsiasi assembly dipendente. Quindi, se il tuo obiettivo, basato su SOLID, è di poter sostituire un'implementazione A con un'implementazione B, mentre la classe C dipendente dall'interfaccia I non deve conoscere la differenza, devi assicurarti che l'assemblaggio con I in esso non cambia, proteggendo così gli usi.

"Ma è solo una ricompilazione" Ti sento protestare. Beh, potrebbe essere, ma nell'app per smartphone, che è più facile sulla larghezza di banda dei dati degli utenti; scaricando un binario che è cambiato o scaricando quel binario e altri cinque con codice che dipende da esso? Non tutti i programmi sono scritti per essere utilizzati dai computer desktop su una LAN. Anche in quel caso, dove la larghezza di banda e la memoria sono economiche, i rilasci di patch più piccoli possono avere valore perché sono banali da inviare all'intera LAN attraverso Active Directory o livelli di gestione del dominio simili; i tuoi utenti aspetteranno solo pochi secondi per essere applicato al successivo accesso invece di pochi minuti per la reinstallazione dell'intera cosa. Per non parlare del fatto che, minore è il numero di assiemi che devono essere ricompilati durante la costruzione di un progetto, più velocemente sarà costruito,

Ora, il disclaimer: questo non è sempre possibile o fattibile da fare. Il modo più semplice per farlo è creare un progetto centralizzato di "interfacce". Questo ha i suoi lati negativi; il codice diventa meno riutilizzabile perché è necessario fare riferimento al progetto di interfaccia E al progetto di implementazione in altre app riutilizzando il livello di persistenza o altri componenti chiave dell'app. Puoi ovviare a questo problema suddividendo le interfacce in assiemi più strettamente accoppiati, ma poi hai più progetti nella tua app che rendono una build completa molto dolorosa. La chiave è l'equilibrio e il mantenimento del design liberamente accoppiato; di solito puoi spostare i file in base alle necessità, quindi quando vedi che una classe avrà bisogno di molte modifiche o che nuove implementazioni di un'interfaccia saranno necessarie regolarmente (forse per interfacciarsi con le versioni di altri software supportate di recente,


8

Perché avere file separati è un disagio? Per me è molto più ordinato e pulito. È comune creare una sottocartella denominata "Interfacce" e attaccare lì i file IFooServer.cs, se si desidera visualizzare un numero inferiore di file in Esplora soluzioni.

Il motivo per cui l'interfaccia è definita nel proprio file è per lo stesso motivo per cui le classi sono generalmente definite nel proprio file: la gestione del progetto è più semplice quando la struttura logica e la struttura dei file sono identiche, quindi si sa sempre quale file è definito in una determinata classe Questo può semplificarti la vita durante il debug (le tracce dello stack delle eccezioni in genere forniscono i file e i numeri di riga) o quando unisci il codice sorgente in un repository di controllo del codice sorgente.


6

Spesso è buona norma lasciare che i file di codice contengano solo una singola classe o una singola interfaccia. Ma queste pratiche di codifica sono un mezzo per raggiungere un fine: strutturare meglio il tuo codice facilitando il lavoro. Se tu e il tuo team trovate più facile lavorare con se le classi sono tenute insieme alle loro interfacce, lo fate sicuramente.

Personalmente preferisco avere l'interfaccia e la classe nello stesso file quando esiste solo una classe che implementa l'interfaccia, come nel tuo caso.

Per quanto riguarda i problemi che hai con la navigazione, posso consigliare vivamente ReSharper . Contiene alcune scorciatoie molto utili per passare direttamente al metodo che implementa un metodo di interfaccia specifico.


3
+1 per indicare che le pratiche del team dovrebbero dettare la struttura dei file e per avere l'approccio contrario alla norma.

3

Ci sono molte volte in cui vuoi che la tua interfaccia non sia solo in un file separato dalla classe, ma anche in un assieme separato .

Ad esempio, un'interfaccia del contratto di servizio WCF può essere condivisa sia dal client che dal servizio se si ha il controllo su entrambe le estremità del filo. Spostando l'interfaccia nel proprio assembly, avrà un minor numero di dipendenze di assembly. Questo rende molto più semplice il consumo da parte del cliente, allentando l'accoppiamento con la sua implementazione.


2

Raramente, se mai, ha senso avere un'unica implementazione di un'interfaccia 1 . Se si inserisce un'interfaccia pubblica e una classe pubblica che implementa tale interfaccia nello stesso file, è probabile che non sia necessaria un'interfaccia.

Quando la classe che co-localizzi con l'interfaccia è astratta e sai che tutte le implementazioni della tua interfaccia dovrebbero ereditare quella classe astratta, localizzare i due nello stesso file ha senso. Dovresti comunque esaminare attentamente la tua decisione di utilizzare un'interfaccia: chiediti se una classe astratta da sola andrebbe bene e lascia cadere l'interfaccia se la risposta è affermativa.

In generale, tuttavia, è necessario attenersi alla strategia "una classe / interfaccia pubblica corrisponde a un file": è facile da seguire e rende più semplice la navigazione dell'albero dei sorgenti.


1 Un'eccezione notevole è quando è necessario disporre di un'interfaccia a scopo di test, poiché la scelta del framework di derisione ha inserito questo requisito aggiuntivo nel codice.


3
È in effetti un modello abbastanza normale avere un'interfaccia per una classe in .NET, in quanto consente ai test unitari di sostituire le dipendenze con mock, stub, spy o altri doppi di test.
Pete,

2
@Pete Ho modificato la mia risposta per includerla come nota. Non definirei questo schema "normale", perché lascia che la tua testabilità riguardi "fughe" nella tua base di codice principale. I moderni quadri di derisione ti aiutano a superare questo problema in larga misura, ma avere un'interfaccia lì semplifica decisamente molto le cose.
dasblinkenlight,

2
@KeithS Una classe senza interfaccia dovrebbe essere nella tua progettazione solo quando sei assolutamente sicuro che non abbia alcun senso sottoclassarla e tu la crei sealed. Se sospetti di poter aggiungere una seconda sottoclasse in futuro, inserirai subito un'interfaccia. PS Un assistente di refactoring di qualità decente può essere tuo a un prezzo modesto di una singola licenza ReSharper :)
dasblinkenlight

1
@KeithS E sì, tutte le classi che non sono specificamente progettate per la sottoclasse dovrebbero essere sigillate. In effetti, le classi dei desideri sono state sigillate per impostazione predefinita, costringendoti a designare le classi per l'ereditarietà in modo esplicito, allo stesso modo in cui il linguaggio attualmente ti costringe a designare funzioni per l'override contrassegnandole virtual.
dasblinkenlight,

2
@KeithS: cosa succede quando la tua implementazione diventa due? Lo stesso di quando l'implementazione cambia; si modifica il codice per riflettere tale modifica. YAGNI si applica qui. Non saprai mai cosa cambierà in futuro, quindi fai la cosa più semplice possibile. (Sfortunatamente i test falliscono con questa massima)
Groky l'

2

Le interfacce appartengono ai loro clienti e non alle loro implementazioni, come definito in Principi, pratiche e schemi agili di Robert C. Martin. Pertanto, combinare interfaccia e implementazione nello stesso posto è contro il suo principio.

Il codice client dipende dall'interfaccia. Ciò consente la possibilità di compilare e distribuire il codice client e l'interfaccia senza l'implementazione. Quindi puoi avere implementazioni diverse che funzionano come plugin per il codice client.

AGGIORNAMENTO: Questo non è un principio solo agile qui. The Gang of Four in Design Patterns, già nel '94, sta già parlando di client che aderiscono alle interfacce e programmano le interfacce. La loro visione è simile.


1
L'uso delle interfacce va ben oltre il dominio che possiede Agile. Agile è una metodologia di sviluppo che utilizza i costrutti del linguaggio in modi particolari. Un'interfaccia è un costrutto di linguaggio.

1
Sì, ma l'interfaccia appartiene ancora al client e non all'implementazione, indipendentemente dal fatto che stiamo parlando di agile o no.
Patkos Csaba,

Sono con te su questo.
Marjan Venema,

0

Personalmente non mi piace l'io davanti a un'interfaccia. Come utente di questo tipo non voglio vedere che questa è un'interfaccia o no. Il tipo è la cosa interessante. In termini di dipendenze FooService è UNA possibile implementazione di IFooService. L'interfaccia dovrebbe trovarsi vicino al luogo in cui viene utilizzata. D'altra parte l'implementazione dovrebbe essere in un luogo in cui può essere facilmente modificata senza influenzare il client. Quindi, preferirei due file - normalmente.


2
Come utente, voglio sapere se un tipo è un'interfaccia, perché ad esempio significa che non posso usarlo new, ma d'altra parte, posso usarlo co-o contro-variantly. Ed Iè una convenzione di denominazione consolidata in .Net, quindi è una buona ragione per aderirvi, anche solo per coerenza con altri tipi.
svick

Iniziare con Iin interface è stata solo un'introduzione ai miei argomenti sulla separazione in due file. Il codice è meglio leggibile e rende più chiara l'inversione delle dipendenze.
ollins,

4
Piaccia o no, l'uso di una "I" davanti a un nome di interfaccia è uno standard di fatto in .NET. Non seguire lo standard in un progetto .NET sarebbe dal mio punto di vista una violazione del "principio del minimo stupore".
Pete,

Il mio punto principale è: separato in due file. Uno per l'astrazione e uno per l'implementazione. Se l'uso di Iè normale nel tuo ambiente: usalo! (Ma non mi piace :))
ollins l'

E nel caso di P.SE, il prefisso I davanti a un'interfaccia rende più evidente che il poster parla di un'interfaccia, non ereditata da un'altra classe.

0

La codifica per le interfacce può essere negativa, in effetti viene trattata come un anti-modello per determinati contesti e non è una legge. Di solito un buon esempio di codifica per interfacce anti-pattern è quando hai un'interfaccia che ha solo un'implementazione nella tua applicazione. Un altro sarebbe se il file di interfaccia diventa così fastidioso che devi nascondere la sua dichiarazione nel file della classe implementata.


avere una sola implementazione nell'applicazione di un'interfaccia non è necessariamente una cosa negativa; si potrebbe proteggere l'applicazione dal cambiamento tecnologico. Ad esempio, un'interfaccia per una tecnologia di persistenza dei dati. Avresti solo una implementazione per l'attuale tecnologia di persistenza dei dati proteggendo al contempo l'app se dovesse cambiare. Quindi ci sarebbe un solo imp in quel momento.
TSmith,

0

Mettere una classe e un'interfaccia nello stesso file non pone limiti al modo in cui entrambi possono essere usati. Un'interfaccia può ancora essere usata per simulazioni ecc. Allo stesso modo, anche se si trova nello stesso file di una classe che la implementa. La domanda potrebbe essere percepita come una questione di convenienza organizzativa, nel qual caso direi di fare qualunque cosa ti semplifichi la vita!

Naturalmente, si potrebbe sostenere che avere i due nello stesso file potrebbe indurre gli incauti a considerarli accoppiati più programmaticamente di quanto non siano realmente, il che è un rischio.

Personalmente, se mi imbattessi in una base di codice che utilizzava una convenzione sull'altra, non mi preoccuperei troppo.


Non è solo una "convenienza organizzativa" ... è anche una questione di gestione del rischio, implementazione di MGMT, tracciabilità del codice, controllo delle modifiche, rumore di controllo del codice sorgente, sicurezza di gestione. Ci sono molti punti di vista su questa domanda.
TSmith,
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.