L'intero punto di un'interfaccia a cui più classi aderiscono a un insieme di regole e implementazioni?
L'intero punto di un'interfaccia a cui più classi aderiscono a un insieme di regole e implementazioni?
Risposte:
A rigor di termini, no non lo fai, si applica YAGNI . Detto questo, il tempo che impiegherai a creare l'interfaccia è minimo, soprattutto se hai uno strumento di generazione del codice utile che fa la maggior parte del lavoro per te. Se non sei sicuro che tu abbia bisogno dell'interfaccia o meno, direi che è meglio sbagliare a sostegno della definizione di un'interfaccia.
Inoltre, l'uso di un'interfaccia anche per una singola classe ti fornirà un'altra implementazione finta per i test unitari, uno che non è in produzione. La risposta di Avner Shahar-Kashtan si espande su questo punto.
Risponderei che la necessità o meno di un'interfaccia non dipende da quante classi la implementeranno. Le interfacce sono uno strumento per la definizione di contratti tra più sottosistemi dell'applicazione; quindi ciò che conta davvero è come la tua applicazione è divisa in sottosistemi. Dovrebbero esserci interfacce come front-end per sottosistemi incapsulati, indipendentemente da quante classi li implementano.
Ecco una regola empirica molto utile:
Foo
riferimento direttamente alla classe BarImpl
, ti impegni fortemente a cambiare Foo
ogni volta che cambi BarImpl
. In pratica, li stai trattando come un'unità di codice suddivisa in due classi.Foo
riferimento all'interfaccia Bar
, ti impegni invece a evitare modifiche a Foo
quando cambi BarImpl
.Se definisci le interfacce nei punti chiave della tua applicazione, rifletti attentamente sui metodi che dovrebbero supportare e quali non dovrebbero, e commenti chiaramente le interfacce per descrivere come dovrebbe comportarsi un'implementazione (e come no), l'applicazione sarà molto più facile da capire perché queste interfacce commentato forniranno una sorta di specificazione della domanda-una descrizione di come è destinato a comportarsi. Questo rende molto più facile leggere il codice (invece di chiedere "cosa diavolo dovrebbe fare questo codice" puoi chiedere "come fa questo codice a fare quello che dovrebbe fare").
Oltre a tutto ciò (o proprio per questo), le interfacce promuovono la compilazione separata. Poiché le interfacce sono banali da compilare e hanno meno dipendenze rispetto alle loro implementazioni, significa che se si scrive classe Foo
per utilizzare un'interfaccia Bar
, di solito è possibile ricompilare BarImpl
senza dover ricompilare Foo
. Nelle applicazioni di grandi dimensioni ciò può far risparmiare molto tempo.
Foo
dipende interfaccia Bar
quindi BarImpl
può essere modificato senza dover ricompilare Foo
. 4. Controllo degli accessi più preciso rispetto alle offerte pubbliche / private (esporre la stessa classe a due client attraverso interfacce diverse).
If you make class Foo refer directly to class BarImpl, you're strongly committing yourself to change Foo every time you change BarImpl
Quando si cambia BarImpl, quali sono le modifiche che è possibile evitare in Foo utilizzando interface Bar
? Dal momento che le firme e le funzionalità di un metodo non cambiano in BarImpl, Foo non richiederà cambiamenti anche senza interfaccia (l'intero scopo dell'interfaccia). Sto parlando dello scenario in cui solo una classe, ad esempio, BarImpl
implementa Bar. Per uno scenario multi-classe, capisco il principio di inversione di dipendenza e come l'interfaccia è utile.
Le interfacce sono designate per definire un comportamento, ovvero un insieme di prototipi di funzioni / metodi. I tipi che implementano l'interfaccia implementeranno quel comportamento, quindi quando hai a che fare con un tale tipo sai (parzialmente) quale comportamento ha.
Non è necessario definire un'interfaccia se si sa che il comportamento da essa definito verrà utilizzato una sola volta. BACIO (mantienilo semplice, stupido)
Mentre in teoria non dovresti avere un'interfaccia solo per amor di un'interfaccia, la risposta di Yannis Rizos suggerisce ulteriori complicazioni:
Quando si scrivono unit test e si usano framework simulati come Moq o FakeItEasy (per nominare i due più recenti che ho usato), si crea implicitamente un'altra classe che implementa l'interfaccia. La ricerca del codice o l'analisi statica potrebbe affermare che esiste solo un'implementazione, ma in realtà c'è l'implementazione fittizia interna. Ogni volta che inizi a scrivere simulazioni, scoprirai che estrarre interfacce ha senso.
Ma aspetta, c'è di più. Esistono più scenari in cui esistono implementazioni di interfaccia implicite. L'uso dello stack di comunicazione WCF di .NET, ad esempio, genera un proxy per un servizio remoto che, di nuovo, implementa l'interfaccia.
In un ambiente di codice pulito, sono d'accordo con il resto delle risposte qui. Tuttavia, presta attenzione a eventuali framework, schemi o dipendenze che potrebbero utilizzare interfacce.
No, non ne hai bisogno e lo considero un anti-pattern per creare automaticamente interfacce per ogni riferimento di classe.
La realizzazione di Foo / FooImpl comporta costi reali per tutto. L'IDE può creare l'interfaccia / implementazione gratuitamente, ma quando si naviga nel codice, si ha il carico cognitivo aggiuntivo da F3 / F12 nel foo.doSomething()
portarsi alla firma dell'interfaccia e non all'implementazione effettiva desiderata. Inoltre hai il disordine di due file invece di uno per tutto.
Quindi dovresti farlo solo quando ne hai davvero bisogno per qualcosa.
Affrontando ora i controargomenti:
Ho bisogno di interfacce per i framework di iniezione delle dipendenze
Le interfacce per supportare i framework sono eredità. In Java, le interfacce erano un requisito per i proxy dinamici, pre-CGLIB. Oggi di solito non ne hai bisogno. È considerato un progresso e un vantaggio per la produttività degli sviluppatori che non ti servono più in EJB3, Spring, ecc.
Ho bisogno di beffe per i test unitari
Se scrivi le tue beffe e hai due implementazioni effettive, allora un'interfaccia è appropriata. Probabilmente non avremmo questa discussione in primo luogo se la tua base di codice avesse sia un FooImpl che un TestFoo.
Ma se stai usando un framework beffardo come Moq, EasyMock o Mockito, puoi deridere le classi e non hai bisogno di interfacce. È analogo all'impostazione foo.method = mockImplementation
in un linguaggio dinamico in cui è possibile assegnare metodi.
Abbiamo bisogno di interfacce per seguire il principio di inversione di dipendenza (DIP)
DIP afferma che costruisci per dipendere da contratti (interfacce) e non da implementazioni. Ma una classe è già un contratto e un'astrazione. Ecco a cosa servono le parole chiave pubbliche / private. All'università, l'esempio canonico era qualcosa di simile a una classe Matrix o Polinomiale: i consumatori hanno un'API pubblica per creare matrici, aggiungerle ecc., Ma non possono preoccuparsi se la matrice è implementata in forma sparsa o densa. Non era richiesto IMatrix o MatrixImpl per dimostrare tale punto.
Inoltre, DIP è spesso applicato in modo eccessivo a tutti i livelli di chiamata di classe / metodo, non solo ai principali limiti del modulo. Un segno che stai applicando eccessivamente DIP è che l'interfaccia e l'implementazione cambiano in fase di blocco in modo tale che devi toccare due file per apportare una modifica anziché una. Se DIP viene applicato in modo appropriato, significa che la tua interfaccia non dovrebbe cambiare spesso. Inoltre, un altro segno è che la tua interfaccia ha solo un consumatore reale (la sua stessa applicazione). Storia diversa se stai costruendo una libreria di classi per il consumo in molte app diverse.
Questo è un corollario del punto di zio Bob Martin sul deridere: dovresti solo deridere i principali confini architettonici. In una webapp l'accesso HTTP e DB sono i limiti principali. Tutte le chiamate di classe / metodo non lo sono. Lo stesso vale per DIP.
Guarda anche:
new Mockup<YourClass>() {}
e l'intera classe, compresi i suoi costruttori, viene derisa, che sia un'interfaccia, una classe astratta o una classe concreta. Puoi anche "sovrascrivere" il comportamento del costruttore se lo desideri. Suppongo che ci siano modi equivalenti in Mockito o Powermock.
Sembra che le risposte su entrambi i lati della recinzione possano essere riassunte in questo:
Progetta bene e metti interfacce dove sono necessarie interfacce.
Come ho notato nella mia risposta alla risposta di Yanni , non penso che tu possa mai avere una regola rigida e veloce sulle interfacce. La regola deve essere, per definizione, flessibile. La mia regola sulle interfacce è che un'interfaccia dovrebbe essere usata ovunque tu stia creando un'API. E un'API dovrebbe essere creata ovunque tu attraversi il confine da un dominio di responsabilità a un altro.
Per un esempio (orribilmente inventato), supponiamo che tu stia costruendo una Car
classe. Nella tua classe, avrai sicuramente bisogno di un livello UI. In questo particolare esempio, prende la forma di un IginitionSwitch
, SteeringWheel
, GearShift
, GasPedal
, e BrakePedal
. Poiché questa macchina contiene un AutomaticTransmission
, non è necessario un ClutchPedal
. (E dal momento che questa è una macchina terribile, non c'è A / C, radio o sedile. È un dato di fatto, anche le assi del pavimento mancano - devi solo aggrapparti a quel volante e sperare per il meglio!)
Quindi quale di queste classi ha bisogno di un'interfaccia? La risposta potrebbe essere tutta, o nessuna, a seconda del progetto.
Potresti avere un'interfaccia simile a questa:
Interface ICabin
Event IgnitionSwitchTurnedOn()
Event IgnitionSwitchTurnedOff()
Event BrakePedalPositionChanged(int percent)
Event GasPedalPositionChanged(int percent)
Event GearShiftGearChanged(int gearNum)
Event SteeringWheelTurned(float degree)
End Interface
A quel punto, il comportamento di quelle classi diventa parte dell'ICabin Interface / API. In questo esempio le classi (se ce ne sono alcune) sono probabilmente semplici, con poche proprietà e una funzione o due. E ciò che stai implicitamente affermando con il tuo design è che queste classi esistono solo per supportare qualsiasi implementazione concreta di ICabin che hai, e non possono esistere da sole, o sono insignificanti al di fuori del contesto ICabin.
È lo stesso motivo per cui non testate i membri privati delle unità: esistono solo per supportare l'API pubblica e quindi il loro comportamento dovrebbe essere testato testando l'API.
Quindi, se la tua classe esiste solo per supportare un'altra classe, e concettualmente la vedi come se non avesse davvero il proprio dominio, allora va bene saltare l'interfaccia. Ma se la tua classe è abbastanza importante da considerarla abbastanza grande da avere il proprio dominio, allora vai avanti e dagli un'interfaccia.
MODIFICARE:
Spesso (incluso in questa risposta) leggerai cose come "dominio", "dipendenza" (spesso associato a "iniezione") che non significano nulla per te quando inizi a programmare (sicuramente non significhi qualcosa per me). Per dominio, significa esattamente come suona:
Il territorio su cui viene esercitato il dominio o l'autorità; i possedimenti di un sovrano o di un commonwealth, o simili. Utilizzato anche in senso figurato. [WordNet sense 2] [1913 Webster]
Nei termini del mio esempio, consideriamo il IgnitionSwitch
. In un'auto spaziale, l'interruttore di accensione è responsabile di:
Queste proprietà costituiscono il dominio del IgnitionSwitch
, o in altre parole, ciò che conosce ed è responsabile.
Il IgnitionSwitch
non è responsabile per il GasPedal
. L'interruttore di accensione ignora completamente il pedale del gas in ogni modo. Entrambi funzionano in modo completamente indipendente l'uno dall'altro (anche se un'auto sarebbe abbastanza inutile senza entrambi!).
Come ho affermato in origine, dipende dal tuo design. È possibile progettare un con IgnitionSwitch
due valori: On ( True
) e Off ( False
). Oppure potresti progettarlo per autenticare la chiave fornita e una miriade di altre azioni. Questa è la parte difficile dell'essere uno sviluppatore nel decidere dove tracciare le linee nella sabbia - e onestamente il più delle volte è completamente relativo. Quelle linee nella sabbia sono tuttavia importanti: è lì che si trova la tua API e quindi dove dovrebbero essere le tue interfacce.
Da MSDN :
Le interfacce sono più adatte alle situazioni in cui le applicazioni richiedono molti tipi di oggetti possibilmente non correlati per fornire determinate funzionalità.
Le interfacce sono più flessibili delle classi di base perché è possibile definire un'unica implementazione in grado di implementare più interfacce.
Le interfacce sono migliori in situazioni in cui non è necessario ereditare l'implementazione da una classe base.
Le interfacce sono utili nei casi in cui non è possibile utilizzare l'ereditarietà delle classi. Ad esempio, le strutture non possono ereditare dalle classi, ma possono implementare interfacce.
Generalmente nel caso di una singola classe, non sarà necessario implementare un'interfaccia, ma considerando il futuro del tuo progetto, potrebbe essere utile definire formalmente il comportamento necessario delle classi.
Per rispondere alla domanda: c'è di più.
Un aspetto importante di un'interfaccia è l'intento.
Un'interfaccia è "un tipo astratto che non contiene dati, ma espone comportamenti" - Interfaccia (informatica) Quindi se questo è un comportamento, o un insieme di comportamenti, che la classe supporta, allora un'interfaccia è probabilmente il modello corretto. Se, tuttavia, il comportamento (i) è (sono) intrinseco al concetto incarnato dalla classe, allora probabilmente non vuoi affatto un'interfaccia.
La prima domanda da porsi è qual è la natura della cosa o del processo che stai cercando di rappresentare. Quindi continua con le ragioni pratiche per attuare quella natura in un determinato modo.
Da quando hai posto questa domanda, suppongo che tu abbia già visto gli interessi di avere un'interfaccia che nasconde più implementazioni. Ciò può essere manifestato dal principio di inversione di dipendenza.
Tuttavia, la necessità di avere un'interfaccia o meno non dipende dal numero delle sue implementazioni. Il vero ruolo di un'interfaccia è che definisce un contratto indicando quale servizio dovrebbe essere fornito invece di come dovrebbe essere implementato.
Una volta definito il contratto, due o più team possono lavorare in modo indipendente. Supponiamo che tu stia lavorando su un modulo A e dipende dal modulo B, il fatto di creare un'interfaccia su B consente di continuare il tuo lavoro senza preoccuparti dell'implementazione di B perché tutti i dettagli sono nascosti dall'interfaccia. Pertanto, la programmazione distribuita diventa possibile.
Anche se il modulo B ha una sola implementazione della sua interfaccia, l'interfaccia è ancora necessaria.
In conclusione, un'interfaccia nasconde i dettagli di implementazione dai suoi utenti. La programmazione per l'interfaccia aiuta a scrivere più documenti perché è necessario definire un contratto, scrivere software più modulare, promuovere test unitari e accelerare la velocità di sviluppo.
Tutte le risposte qui sono molto buone. In effetti, la maggior parte delle volte non è necessario implementare un'interfaccia diversa. Ma ci sono casi in cui si potrebbe desiderare di farlo comunque. Ecco qualche caso in cui lo faccio:
La classe implementa un'altra interfaccia che non voglio esporre
Happen spesso con la classe adattatore che collega il codice di terze parti.
interface NameChangeListener { // Implemented by a lot of people
void nameChanged(String name);
}
interface NameChangeCount { // Only implemented by my class
int getCount();
}
class NameChangeCounter implements NameChangeListener, NameChangeCount {
...
}
class SomeUserInterface {
private NameChangeCount currentCount; // Will never know that you can change the counter
}
La classe usa una tecnologia specifica che non dovrebbe trapelare
principalmente quando interagisce con librerie esterne. Anche se esiste una sola implementazione, utilizzo un'interfaccia per assicurarmi di non introdurre accoppiamenti non necessari con la libreria esterna.
interface SomeRepository { // Guarantee that the external library details won't leak trough
...
}
class OracleSomeRepository implements SomeRepository {
... // Oracle prefix allow us to quickly know what is going on in this class
}
Comunicazione a più livelli
Anche se solo una classe UI implementa mai una della classe di dominio, consente una migliore separazione tra tali livelli e, soprattutto, evita la dipendenza ciclica.
package project.domain;
interface UserRequestSource {
public UserRequest getLastRequest();
}
class UserBehaviorAnalyser {
private UserRequestSource requestSource;
}
package project.ui;
class OrderCompleteDialog extends SomeUIClass implements project.domain.UserRequestSource {
// UI concern, no need for my domain object to know about this method.
public void displayLabelInErrorMode();
// They most certainly need to know about *that* though
public UserRequest getLastRequest();
}
Per la maggior parte degli oggetti dovrebbe essere disponibile solo un sottoinsieme del metodo.
Principalmente accade quando ho un metodo di configurazione sulla classe concreta
interface Sender {
void sendMessage(Message message)
}
class PacketSender implements Sender {
void sendMessage(Message message);
void setPacketSize(int sizeInByte);
}
class Throttler { // This class need to have full access to the object
private PacketSender sender;
public useLowNetworkUsageMode() {
sender.setPacketSize(LOW_PACKET_SIZE);
sender.sendMessage(new NotifyLowNetworkUsageMessage());
... // Other details
}
}
class MailOrder { // Not this one though
private Sender sender;
}
Quindi alla fine uso l'interfaccia per lo stesso motivo per cui utilizzo il campo privato: altri oggetti non dovrebbero avere accesso a cose a cui non dovrebbero accedere. Se ho un caso del genere, presento un'interfaccia anche se solo una classe la implementa.
Le interfacce sono davvero importanti, ma prova a controllarne il numero.
Avendo intrapreso la strada della creazione di interfacce per quasi tutto, è facile finire con il codice degli "spaghetti tritati". Rinvio alla maggiore saggezza di Ayende Rahien che ha pubblicato alcune parole molto sagge sull'argomento:
http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application
Questo è il suo primo post di un'intera serie, quindi continua a leggere!
Uno dei motivi per cui potresti voler introdurre un'interfaccia in questo caso è seguire il principio di inversione di dipendenza . Cioè, il modulo che utilizza la classe dipenderà da un'astrazione di essa (cioè l'interfaccia) anziché da un'implementazione concreta. Disaccoppia i componenti di alto livello dai componenti di basso livello.
Non c'è alcun motivo reale per fare qualcosa. Le interfacce servono per aiutarti e non per il programma di output. Quindi, anche se l'interfaccia è implementata da un milione di classi, non esiste una regola che dice che devi crearne una. Ne crei uno in modo che quando tu o chiunque altro usi il tuo codice, desideri cambiare qualcosa che percola a tutte le implementazioni. La creazione di un'interfaccia ti aiuterà in tutti i casi futuri in cui potresti voler creare un'altra classe che la implementa.
Non è sempre necessario definire un'interfaccia per una classe.
Oggetti semplici come oggetti valore non hanno implementazioni multiple. Non hanno nemmeno bisogno di essere derisi. L'implementazione può essere testata da sola e quando vengono testate altre classi che dipendono da esse, è possibile utilizzare l'oggetto valore reale.
Ricorda che la creazione di un'interfaccia ha un costo. Deve essere aggiornato lungo l'implementazione, ha bisogno di un file aggiuntivo e alcuni IDE avranno problemi di zoom dell'implementazione, non dell'interfaccia.
Quindi definirei interfacce solo per classi di livello superiore, dove desideri un'astrazione dall'implementazione.
Nota che con una classe ottieni un'interfaccia gratis. Oltre all'implementazione, una classe definisce un'interfaccia dall'insieme di metodi pubblici. Tale interfaccia è implementata da tutte le classi derivate. Non si tratta strettamente di un'interfaccia, ma può essere utilizzata esattamente allo stesso modo. Quindi non penso che sia necessario ricreare un'interfaccia che esiste già sotto il nome della classe.