Cosa significa "programmare su un'interfaccia"?


814

Ho visto questo menzionato alcune volte e non sono chiaro su cosa significhi. Quando e perché dovresti farlo?

So cosa fanno le interfacce, ma il fatto che non sia chiaro su questo mi fa pensare che mi sto perdendo nel usarle correttamente.

È solo così se dovessi fare:

IInterface classRef = new ObjectWhatever()

Potresti usare qualsiasi classe che implementa IInterface? Quando dovresti farlo? L'unica cosa che mi viene in mente è se hai un metodo e non sei sicuro di quale oggetto verrà passato se non per l'implementazione IInterface. Non riesco a pensare quanto spesso dovresti farlo.

Inoltre, come potresti scrivere un metodo che accetta un oggetto che implementa un'interfaccia? È possibile?


3
Se ricordi e il tuo programma deve essere ottimale, poco prima della compilazione potresti voler scambiare la dichiarazione dell'interfaccia con l'implementazione effettiva. L'uso di un'interfaccia aggiunge un livello di riferimento indiretto che dà un impatto sulle prestazioni. Distribuisci il tuo codice programmato sulle interfacce però ...
Ande Turner,

18
@Ande Turner: questo è un consiglio scadente. 1). "Il tuo programma deve essere ottimale" non è una buona ragione per scambiare le interfacce! Quindi dici "Distribuisci il tuo codice programmato sulle interfacce però ..." quindi stai avvisando che dato il requisito (1) rilasci quindi un codice non ottimale?!?
Mitch Wheat,

74
La maggior parte delle risposte qui non sono del tutto esatte. Non significa affatto o nemmeno implica "usa la parola chiave dell'interfaccia". Un'interfaccia è una specifica di come usare qualcosa - sinonimo del contratto (cercare). Separata da ciò è l'attuazione, che è come viene adempiuto quel contratto. Programmare solo contro le garanzie del metodo / tipo in modo che, quando il metodo / tipo viene modificato in modo tale da obbedire al contratto, non violi il codice che lo utilizza.
jyoungdev,

2
@ apollodude217 che è in realtà la migliore risposta su tutta la pagina. Almeno per la domanda nel titolo, dal momento che ci sono almeno 3 domande abbastanza diverse qui ...
Andrew Spencer,

4
Il problema fondamentale con domande come questa è che si presume che "programmare su un'interfaccia" significhi "avvolgere tutto in un'interfaccia astratta", il che è sciocco se si considera che il termine precede il concetto di interfacce astratte in stile Java.
Jonathan Allen,

Risposte:


1634

Ci sono alcune risposte meravigliose qui a queste domande che entrano in ogni sorta di grande dettaglio su interfacce e codice di accoppiamento vagamente, inversione di controllo e così via. Ci sono alcune discussioni piuttosto inebrianti, quindi vorrei cogliere l'occasione per scomporre un po 'le cose per capire perché un'interfaccia è utile.

Quando ho iniziato a essere esposto alle interfacce, anch'io ero confuso riguardo alla loro rilevanza. Non capivo perché ne avessi bisogno. Se stiamo usando un linguaggio come Java o C #, abbiamo già l'ereditarietà e ho visto le interfacce come una forma più debole di eredità e ho pensato: "perché preoccuparsi?" In un certo senso avevo ragione, puoi pensare alle interfacce come a una sorta di debole forma di eredità, ma oltre a ciò ho finalmente compreso il loro uso come costrutto linguistico pensando a loro come un mezzo per classificare tratti o comportamenti comuni che sono stati esposti da potenzialmente molte classi di oggetti non correlate.

Ad esempio, supponi di avere un gioco SIM e di seguire le seguenti classi:

class HouseFly inherits Insect {
    void FlyAroundYourHead(){}
    void LandOnThings(){}
}

class Telemarketer inherits Person {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}
}

Chiaramente, questi due oggetti non hanno nulla in comune in termini di eredità diretta. Ma potresti dire che sono entrambi fastidiosi.

Diciamo che il nostro gioco deve avere una sorta di cosa casuale che infastidisce il giocatore quando mangiano la cena. Questo potrebbe essere un HouseFlyo uno Telemarketero entrambi - ma come si consente ad entrambi con una singola funzione? E come chiedi a ogni diverso tipo di oggetto di "fare le loro cose fastidiose" allo stesso modo?

La chiave per rendersi conto è che sia a Telemarketerche HouseFlycondividere un comportamento comune interpretato vagamente, anche se non sono nulla di simile in termini di modellarli. Quindi, creiamo un'interfaccia che entrambi possano implementare:

interface IPest {
    void BeAnnoying();
}

class HouseFly inherits Insect implements IPest {
    void FlyAroundYourHead(){}
    void LandOnThings(){}

    void BeAnnoying() {
        FlyAroundYourHead();
        LandOnThings();
    }
}

class Telemarketer inherits Person implements IPest {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}

    void BeAnnoying() {
        CallDuringDinner();
        ContinueTalkingWhenYouSayNo();
    }
}

Ora abbiamo due classi che possono essere ciascuna fastidiose a modo loro. E non hanno bisogno di derivare dalla stessa classe base e condividere caratteristiche intrinseche comuni - devono semplicemente soddisfare il contratto di IPest- quel contratto è semplice. Devi solo BeAnnoying. A questo proposito, possiamo modellare quanto segue:

class DiningRoom {

    DiningRoom(Person[] diningPeople, IPest[] pests) { ... }

    void ServeDinner() {
        when diningPeople are eating,

        foreach pest in pests
        pest.BeAnnoying();
    }
}

Qui abbiamo una sala da pranzo che accetta un numero di commensali e un numero di parassiti - nota l'uso dell'interfaccia. Ciò significa che nel nostro piccolo mondo, un membro pestsdell'array potrebbe effettivamente essere un Telemarketeroggetto o un HouseFlyoggetto.

Il ServeDinnermetodo viene chiamato quando viene servita la cena e la nostra gente nella sala da pranzo dovrebbe mangiare. Nel nostro piccolo gioco, è in quel momento che i nostri parassiti fanno il loro lavoro - a ciascun parassita viene richiesto di essere fastidiosi tramite l' IPestinterfaccia. In questo modo, possiamo facilmente avere entrambi Telemarketersed HouseFlysessere fastidiosi in ciascuno dei loro modi - ci importa solo che abbiamo qualcosa DiningRoomnell'oggetto che è un parassita, non ci interessa davvero di cosa si tratta e non potrebbero avere nulla in comune con gli altri.

Questo esempio di pseudo-codice molto elaborato (che si trascinava molto più a lungo di quanto mi aspettassi) ha semplicemente lo scopo di illustrare il tipo di cosa che alla fine mi ha acceso la luce in termini di quando potremmo usare un'interfaccia. Mi scuso in anticipo per la stupidità dell'esempio, ma spero che ti aiuti a capire. E, per essere sicuri, le altre risposte pubblicate che hai ricevuto qui coprono davvero la gamma dell'uso delle interfacce oggi nei modelli di progettazione e nelle metodologie di sviluppo.


3
Un'altra cosa da considerare è che in alcuni casi potrebbe essere utile avere un'interfaccia per cose che "potrebbero" essere fastidiose e avere una varietà di oggetti implementati BeAnnoyingcome no-op; questa interfaccia può esistere al posto di, o in aggiunta, l'interfaccia per le cose che sono fastidiosi (se esistono entrambe le interfacce, le "cose che sono fastidiosi" interfaccia dovrebbe ereditare dalle "cose che potrebbero essere fastidioso" interfaccia). Lo svantaggio dell'uso di tali interfacce è che le implementazioni possono essere gravate dall'implementazione di un numero "fastidioso" di metodi di stub. Il vantaggio è che ...
supercat

4
I metodi non intendono rappresentare metodi astratti - la loro implementazione è irrilevante per la domanda che si è concentrata sulle interfacce.
Peter Meyer,

33
Comportamenti incapsulanti, come IPest, sono conosciuti come pattern strategici nel caso in cui qualcuno fosse interessato a dare seguito a più materiale sull'argomento ...
nckbrz

9
È interessante notare che non si sottolinea che, poiché gli oggetti in IPest[]sono riferimenti IPest, è possibile chiamare BeAnnoying()perché hanno quel metodo, mentre non è possibile chiamare altri metodi senza un cast. Tuttavia, ogni singolo BeAnnoying()metodo degli oggetti verrà chiamato.
D. Ben Knoble,

4
Molto buona spiegazione ... ho solo bisogno di dirlo qui: non ho mai sentito parlare di interfacce che sono una sorta di meccanismo di ereditarietà sciolto, ma invece io so di eredità essere usato come un povero meccanismo per la definizione delle interfacce (ad esempio, in una regolare Python si fallo sempre).
Carlos H Romano,

283

L'esempio specifico che ho usato per dare agli studenti è che dovrebbero scrivere

List myList = new ArrayList(); // programming to the List interface

invece di

ArrayList myList = new ArrayList(); // this is bad

Questi sembrano esattamente gli stessi in un breve programma, ma se continui a utilizzare myList100 volte nel tuo programma puoi iniziare a vedere la differenza. La prima dichiarazione assicura che si chiamino solo i metodi myListdefiniti Listdall'interfaccia (quindi nessun ArrayListmetodo specifico). Se hai programmato l'interfaccia in questo modo, in seguito puoi decidere di averne davvero bisogno

List myList = new TreeList();

e devi solo cambiare il tuo codice in quel punto. Sai già che il resto del tuo codice non fa nulla che possa essere rotto cambiando l' implementazione perché hai programmato l' interfaccia .

I vantaggi sono ancora più evidenti (penso) quando parli di parametri di metodo e valori di ritorno. Prendi questo per esempio:

public ArrayList doSomething(HashMap map);

Tale dichiarazione del metodo ti lega a due implementazioni concrete ( ArrayListe HashMap). Non appena quel metodo viene chiamato da un altro codice, qualsiasi modifica a questi tipi probabilmente significa che dovrai cambiare anche il codice chiamante. Sarebbe meglio programmare le interfacce.

public List doSomething(Map map);

Ora non importa quale tipo di Listritorno, o quale tipo di Mapè passato come parametro. Le modifiche apportate all'interno del doSomethingmetodo non ti costringeranno a modificare il codice chiamante.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Yvette,

Spiegazione molto chiara. Molto utile
samuel luswata il

Ho una domanda sul motivo che hai citato "La prima dichiarazione assicura che tu chiami solo i metodi su myList che sono definiti dall'interfaccia List (quindi nessun metodo specifico ArrayList). Se hai programmato l'interfaccia in questo modo, in seguito puoi decidere che hai davvero bisogno di List myList = new TreeList (); e devi solo cambiare il tuo codice in quel punto. " Forse ho frainteso, mi chiedo perché hai bisogno di cambiare ArrayList in TreeList se vuoi "assicurarti di chiamare solo metodi su myList"?
user3014901

1
@ user3014901 Esistono diversi motivi per cui potresti voler cambiare il tipo di elenco che stai utilizzando. Uno potrebbe avere prestazioni di ricerca migliori, per esempio. Il punto è che se si programma all'interfaccia Elenco, è più facile cambiare il codice in una diversa implementazione in un secondo momento.
Bill the Lizard,

73

La programmazione su un'interfaccia sta dicendo "Ho bisogno di questa funzionalità e non mi interessa da dove viene".

Prendere in considerazione (in Java), l' Listinterfaccia di contro le ArrayListe LinkedListconcreti classi. Se tutto ciò che mi interessa è che ho una struttura di dati contenente più elementi di dati a cui dovrei accedere tramite iterazione, sceglierei un List(e questo è il 99% delle volte). Se so che ho bisogno di inserire / eliminare a tempo costante da entrambe le estremità dell'elenco, potrei scegliere l' LinkedListimplementazione concreta (o più probabilmente, utilizzare l' interfaccia Queue ). Se so di aver bisogno di un accesso casuale per indice, sceglierei la ArrayListclasse concreta.


1
sono totalmente d'accordo, cioè l'indipendenza tra ciò che viene fatto rispetto a come viene fatto. Partizionando un sistema lungo componenti indipendenti, si ottiene un sistema che è semplice e riutilizzabile (vedi Simple Made Easy del ragazzo che ha creato Clojure)
beluchin

38

L'uso delle interfacce è un fattore chiave per rendere facilmente testabile il tuo codice oltre a rimuovere accoppiamenti non necessari tra le tue classi. Creando un'interfaccia che definisce le operazioni sulla tua classe, permetti alle classi che vogliono usare quella funzionalità la possibilità di usarla senza dipendere direttamente dalla tua classe di implementazione. Se in seguito si decide di modificare e utilizzare un'implementazione diversa, è necessario modificare solo la parte del codice in cui viene implementata l'implementazione. Non è necessario modificare il resto del codice perché dipende dall'interfaccia, non dalla classe di implementazione.

Questo è molto utile nella creazione di unit test. Nella classe sotto test lo fai dipendere dall'interfaccia e iniettare un'istanza dell'interfaccia nella classe (o una factory che gli consente di costruire istanze dell'interfaccia secondo necessità) tramite il costruttore o un settatore di proprietà. La classe utilizza l'interfaccia fornita (o creata) nei suoi metodi. Quando si va a scrivere i test, è possibile deridere o falsificare l'interfaccia e fornire un'interfaccia che risponda ai dati configurati nel test unitario. Puoi farlo perché la tua classe in prova si occupa solo dell'interfaccia, non della tua implementazione concreta. Qualsiasi classe che implementa l'interfaccia, inclusa la tua classe finta o falsa, lo farà.

EDIT: Di seguito è riportato un link a un articolo in cui Erich Gamma discute la sua citazione, "Programma a un'interfaccia, non a un'implementazione".

http://www.artima.com/lejava/articles/designprinciples.html


3
Per favore, rileggi questa intervista: Gamma stava ovviamente parlando del concetto di interfaccia OO, non del tipo speciale di classe JAVA o C # (ISomething). Il problema è che molte persone parlavano della parola chiave, quindi ora abbiamo molte interfacce non richieste (ISomething).
Sylvain Rodrigue,

Intervista molto buona. Per favore, fai attenzione ai futuri lettori, ci sono quattro pagine nell'intervista. Vorrei quasi chiudere il browser prima di vederlo.
Ad Infinitum,

38

La programmazione su un'interfaccia non ha assolutamente nulla a che fare con le interfacce astratte come vediamo in Java o .NET. Non è nemmeno un concetto OOP.

Ciò significa che non andare in giro con gli interni di un oggetto o una struttura di dati. Utilizza l'interfaccia del programma astratta o API per interagire con i tuoi dati. In Java o C # ciò significa utilizzare proprietà e metodi pubblici anziché l'accesso al campo non elaborato. Per C ciò significa utilizzare le funzioni anziché i puntatori non elaborati.

EDIT: E con i database significa usare viste e procedure memorizzate invece dell'accesso diretto alla tabella.


5
Migliore risposta. Gamma fornisce una spiegazione simile qui: artima.com/lejava/articles/designprinciples.html (vedi pagina 2). Si riferisce al concetto di OO ma hai ragione: è più grande di così.
Sylvain Rodrigue,

36

Dovresti esaminare Inversion of Control:

In uno scenario del genere, non scriveresti questo:

IInterface classRef = new ObjectWhatever();

Scriveresti qualcosa del genere:

IInterface classRef = container.Resolve<IInterface>();

Ciò andrebbe in una configurazione basata su regole containernell'oggetto e costruirà l'oggetto reale per te, che potrebbe essere ObjectWhatever. L'importante è che tu possa sostituire questa regola con qualcosa che ha usato del tutto un altro tipo di oggetto, e il tuo codice funzionerebbe comunque.

Se lasciamo IoC fuori dalla tabella, puoi scrivere codice che sa che può parlare con un oggetto che fa qualcosa di specifico , ma non quale tipo di oggetto o come lo fa.

Ciò sarebbe utile quando si passano parametri.

Per quanto riguarda la tua domanda tra parentesi "Inoltre, come potresti scrivere un metodo che accetta un oggetto che implementa un'interfaccia? È possibile?", In C # useresti semplicemente il tipo di interfaccia per il tipo di parametro, come questo:

public void DoSomethingToAnObject(IInterface whatever) { ... }

Questo si inserisce direttamente nel "parlare con un oggetto che fa qualcosa di specifico". Il metodo sopra definito sa cosa aspettarsi dall'oggetto, che implementa tutto in IInterface, ma non gli importa quale tipo di oggetto sia, solo che aderisce al contratto, che è quello che è un'interfaccia.

Ad esempio, probabilmente hai familiarità con i calcolatori e probabilmente ne hai usati parecchi ai tuoi giorni, ma il più delle volte sono tutti diversi. D'altra parte, tu sai come dovrebbe funzionare una calcolatrice standard, quindi sei in grado di usarle tutte, anche se non puoi utilizzare le funzionalità specifiche di ciascuna calcolatrice che nessuna delle altre ha.

Questa è la bellezza delle interfacce. Puoi scrivere un pezzo di codice, che sappia che gli verranno passati degli oggetti da cui può aspettarsi un certo comportamento. Non gliene importa che tipo di oggetto sia, solo che supporta il comportamento necessario.

Lascia che ti faccia un esempio concreto.

Abbiamo un sistema di traduzione personalizzato per moduli Windows. Questo sistema scorre i controlli in un modulo e traduce il testo in ciascuno. Il sistema sa come gestire i controlli di base, come il tipo di controllo che ha una proprietà Text e simili elementi di base, ma per qualsiasi cosa di base, non è all'altezza.

Ora, poiché i controlli ereditano da classi predefinite su cui non abbiamo alcun controllo, potremmo fare una delle tre cose:

  1. Crea supporto per il nostro sistema di traduzione per rilevare in modo specifico con quale tipo di controllo sta lavorando e tradurre i bit corretti (incubo di manutenzione)
  2. Costruire il supporto in classi base (impossibile, poiché tutti i controlli ereditano da diverse classi predefinite)
  3. Aggiungi il supporto dell'interfaccia

Quindi abbiamo fatto il n. 3. Tutti i nostri controlli implementano ILocalizable, che è un'interfaccia che ci offre un metodo, la capacità di tradurre "se stesso" in un contenitore di testo / regole di traduzione. Pertanto, il modulo non ha bisogno di sapere quale tipo di controllo ha trovato, solo che implementa l'interfaccia specifica e sa che esiste un metodo in cui può chiamare per localizzare il controllo.


31
Perché menzionare l'IoC all'inizio perché questo aggiungerebbe solo più confusione.
Kevin Le - Khnle il

1
D'accordo, direi che programmare contro le interfacce è solo una tecnica per rendere l'IoC più semplice e affidabile.
terjetyl,

28

Codice all'interfaccia Non l'implementazione non ha nulla a che fare con Java, né con il suo costrutto Interface.

Questo concetto è stato messo in risalto nei libri Patterns / Gang of Four, ma molto probabilmente era già in circolazione molto prima. Il concetto certamente esisteva ben prima che esistesse Java.

Il costrutto Java Interface è stato creato per aiutare in questa idea (tra le altre cose) e le persone si sono concentrate troppo sul costrutto come centro del significato piuttosto che sull'intento originale. Tuttavia, è la ragione per cui abbiamo metodi e attributi pubblici e privati ​​in Java, C ++, C #, ecc.

Significa solo interagire con un oggetto o l'interfaccia pubblica del sistema. Non ti preoccupare o addirittura anticipare come fa ciò che fa internamente. Non preoccuparti di come viene implementato. Nel codice orientato agli oggetti, è per questo che abbiamo metodi / attributi pubblici contro privati. Abbiamo intenzione di utilizzare i metodi pubblici perché i metodi privati ​​sono lì solo per l'uso interno, all'interno della classe. Costituiscono l'implementazione della classe e possono essere cambiati come richiesto senza cambiare l'interfaccia pubblica. Supponiamo che per quanto riguarda la funzionalità, un metodo su una classe eseguirà la stessa operazione con lo stesso risultato previsto ogni volta che lo chiami con gli stessi parametri. Permette all'autore di cambiare il modo in cui la classe funziona, la sua implementazione, senza interrompere il modo in cui le persone interagiscono con essa.

E puoi programmare sull'interfaccia, non sull'implementazione senza mai usare un costrutto Interface. È possibile programmare per l'interfaccia non l'implementazione in C ++, che non ha un costrutto Interface. È possibile integrare due sistemi aziendali di grandi dimensioni in modo molto più efficace purché interagiscano attraverso interfacce pubbliche (contratti) anziché chiamare metodi su oggetti interni ai sistemi. Le interfacce dovrebbero sempre reagire nello stesso modo previsto, dati gli stessi parametri di input; se implementato sull'interfaccia e non sull'implementazione. Il concetto funziona in molti luoghi.

Scuoti il ​​pensiero che Java Interfaces abbia qualcosa a che fare con il concetto di "Program to the Interface, Not the Implementation". Possono aiutare ad applicare il concetto, ma sono non il concetto.


1
La prima frase dice tutto. Questa dovrebbe essere la risposta accettata.
madumlao,

14

Sembra che tu capisca come funzionano le interfacce ma non sei sicuro di quando usarle e quali vantaggi offrono. Ecco alcuni esempi di quando un'interfaccia avrebbe senso:

// if I want to add search capabilities to my application and support multiple search
// engines such as Google, Yahoo, Live, etc.

interface ISearchProvider
{
    string Search(string keywords);
}

allora potrei creare GoogleSearchProvider, YahooSearchProvider, LiveSearchProvider, ecc.

// if I want to support multiple downloads using different protocols
// HTTP, HTTPS, FTP, FTPS, etc.
interface IUrlDownload
{
    void Download(string url)
}

// how about an image loader for different kinds of images JPG, GIF, PNG, etc.
interface IImageLoader
{
    Bitmap LoadImage(string filename)
}

quindi creare JpegImageLoader, GifImageLoader, PngImageLoader, ecc.

La maggior parte dei componenti aggiuntivi e dei sistemi di plugin funzionano con le interfacce.

Un altro uso popolare è per il modello di repository. Supponiamo di voler caricare un elenco di codici postali da diverse fonti

interface IZipCodeRepository
{
    IList<ZipCode> GetZipCodes(string state);
}

quindi potrei creare un XMLZipCodeRepository, SQLZipCodeRepository, CSVZipCodeRepository, ecc. Per le mie applicazioni Web, spesso creo presto repository XML in modo da poter mettere in funzione qualcosa prima che il database SQL sia pronto. Quando il database è pronto, scrivo un archivio SQL per sostituire la versione XML. Il resto del mio codice rimane invariato poiché viene eseguito esclusivamente da interfacce.

I metodi possono accettare interfacce come:

PrintZipCodes(IZipCodeRepository zipCodeRepository, string state)
{
    foreach (ZipCode zipCode in zipCodeRepository.GetZipCodes(state))
    {
        Console.WriteLine(zipCode.ToString());
    }
}

10

Rende il tuo codice molto più estensibile e più facile da mantenere quando hai insiemi di classi simili. Sono un programmatore junior, quindi non sono un esperto, ma ho appena finito un progetto che ha richiesto qualcosa di simile.

Lavoro sul software lato client che parla con un server che esegue un dispositivo medico. Stiamo sviluppando una nuova versione di questo dispositivo che ha alcuni nuovi componenti che il cliente deve configurare a volte. Esistono due tipi di nuovi componenti e sono diversi, ma sono anche molto simili. Fondamentalmente, ho dovuto creare due moduli di configurazione, due classi di elenchi, due di tutto.

Decisi che sarebbe stato meglio creare una classe base astratta per ciascun tipo di controllo che potesse contenere quasi tutta la logica reale, e quindi derivati ​​i tipi per prendersi cura delle differenze tra i due componenti. Tuttavia, le classi di base non sarebbero state in grado di eseguire operazioni su questi componenti se dovessi preoccuparmi dei tipi per tutto il tempo (beh, avrebbero potuto, ma ci sarebbe stata un'istruzione "if" o cambiare ogni metodo) .

Ho definito una semplice interfaccia per questi componenti e tutte le classi di base parlano a questa interfaccia. Ora, quando cambio qualcosa, praticamente "funziona" ovunque e non ho duplicazioni di codice.


10

Molte spiegazioni là fuori, ma per renderlo ancora più semplice. Prendi ad esempio a List. Si può implementare un elenco con come:

  1. Un array interno
  2. Un elenco collegato
  3. Altre implementazioni

Costruendo su un'interfaccia, dire a List. Codifichi solo la definizione di Elenco o ciò che Listsignifica nella realtà.

È possibile utilizzare internamente qualsiasi tipo di implementazione, ad esempio arrayun'implementazione. Ma supponiamo che desideri cambiare l'implementazione per qualche motivo, per esempio dire un errore o una prestazione. Quindi devi solo cambiare la dichiarazione List<String> ls = new ArrayList<String>()in List<String> ls = new LinkedList<String>().

In nessun'altra parte del codice, dovrai cambiare qualcos'altro; Perché tutto il resto è stato costruito sulla definizione di List.


8

Se programmi in Java, JDBC è un buon esempio. JDBC definisce un set di interfacce ma non dice nulla sull'implementazione. Le tue applicazioni possono essere scritte su questo set di interfacce. In teoria, scegli un driver JDBC e la tua applicazione funzionerebbe. Se scopri che esiste un driver JDBC più veloce o "migliore" o più economico o per qualsiasi motivo, puoi di nuovo in teoria riconfigurare il tuo file di proprietà e senza dover apportare modifiche all'applicazione, l'applicazione funzionerebbe comunque.


Non è utile solo nel caso in cui sia disponibile un driver migliore, ma consente di cambiare completamente i fornitori di database.
Ken Liu,

3
JDBC è così male che deve essere sostituito. Trova un altro esempio.
Joshua,

JDBC è male, ma non per nessun motivo a che fare con interfaccia vs implementazione o livelli di astrazione. E così per illustrare il concetto in questione, è semplicemente perfetto.
Erwin Smout,

8

La programmazione su interfacce è fantastica, promuove l'accoppiamento lento. Come menzionato da @lassevk, Inversion of Control ne è un ottimo uso.

Inoltre, esamina i principi SOLID .ecco una serie di video

Passa attraverso un hard coded (esempio fortemente accoppiato) quindi esamina le interfacce, passando infine a uno strumento IoC / DI (NInject)


7

Sono arrivato in ritardo a questa domanda, ma voglio menzionare qui che la riga "Programma su un'interfaccia, non un'implementazione" ha avuto una buona discussione nel libro GoF (Gang of Four) Design Patterns.

Ha dichiarato, a p. 18:

Programma per un'interfaccia, non un'implementazione

Non dichiarare le variabili come istanze di particolari classi concrete. Invece, esegui il commit solo su un'interfaccia definita da una classe astratta. Scoprirai che questo è un tema comune dei motivi di progettazione in questo libro.

e soprattutto, è iniziato con:

Esistono due vantaggi nel manipolare gli oggetti esclusivamente in termini di interfaccia definita da classi astratte:

  1. I clienti rimangono inconsapevoli dei tipi specifici di oggetti che usano, purché gli oggetti aderiscano all'interfaccia che i clienti si aspettano.
  2. I client rimangono ignari delle classi che implementano questi oggetti. I clienti conoscono solo le classi astratte che definiscono l'interfaccia.

Quindi, in altre parole, non scriverlo nelle tue classi in modo che abbia un quack()metodo per le anatre e quindi un bark()metodo per i cani, perché sono troppo specifici per una particolare implementazione di una classe (o sottoclasse). Invece, scrivi il metodo usando nomi abbastanza generici da essere utilizzati nella classe base, come giveSound()o move(), in modo che possano essere usati per anatre, cani o persino automobili, e quindi il cliente delle tue classi può semplicemente dire .giveSound()piuttosto che pensando se utilizzare quack()o bark()addirittura determinare il tipo prima di emettere il messaggio corretto da inviare all'oggetto.


6

Oltre alla risposta già selezionata (e ai vari post informativi qui), consiglio vivamente di prendere una copia di Head First Design Patterns . È una lettura molto semplice e risponderà direttamente alla tua domanda, spiegherà perché è importante e ti mostrerà molti schemi di programmazione che puoi usare per usare quel principio (e altri).


5

Per aggiungere ai post esistenti, a volte la codifica per le interfacce aiuta su progetti di grandi dimensioni quando gli sviluppatori lavorano contemporaneamente su componenti separati. Tutto ciò che serve è definire le interfacce in anticipo e scrivere loro il codice mentre altri sviluppatori scrivono il codice sull'interfaccia che state implementando.


4

È anche buono per il test unitario, puoi iniettare le tue classi (che soddisfano i requisiti dell'interfaccia) in una classe che dipende da essa


4

Può essere vantaggioso programmare le interfacce, anche quando non dipendiamo dalle astrazioni.

La programmazione alle interfacce ci obbliga a utilizzare un sottoinsieme contestualmente appropriato di un oggetto . Questo aiuta perché:

  1. ci impedisce di fare cose contestualmente inadeguate e
  2. ci consente di modificare in modo sicuro l'implementazione in futuro.

Ad esempio, considera una Personclasse che implementa Friendl' Employeeinterfaccia e.

class Person implements AbstractEmployee, AbstractFriend {
}

Nel contesto del compleanno della persona, programmiamo l' Friendinterfaccia, per evitare di trattare la persona come una Employee.

function party() {
    const friend: Friend = new Person("Kathryn");
    friend.HaveFun();
}

Nel contesto del lavoro della persona, programmiamo l' Employeeinterfaccia, per evitare di confondere i confini del posto di lavoro.

function workplace() {
    const employee: Employee = new Person("Kathryn");
    employee.DoWork();
}

Grande. Ci siamo comportati in modo appropriato in contesti diversi e il nostro software funziona bene.

Lontano nel futuro, se la nostra azienda cambia per lavorare con i cani, possiamo cambiare il software abbastanza facilmente. Innanzitutto, creiamo una Dogclasse che implementa sia Friende Employee. Quindi, passiamo in modo sicuro new Person()a new Dog(). Anche se entrambe le funzioni hanno migliaia di righe di codice, quella semplice modifica funzionerà perché sappiamo che quanto segue è vero:

  1. La funzione partyutilizza solo il Friendsottoinsieme di Person.
  2. La funzione workplaceutilizza solo il Employeesottoinsieme di Person.
  3. Class Dogimplementa sia la Friende Employeeinterfacce.

D'altra parte, se l'uno partyo workplacel'altro avesse programmato contro Person, vi sarebbe il rischio che entrambi avessero un Personcodice specifico. Passare da Persona Dogrichiederebbe di esaminare il codice per estirpare qualsiasi Personcodice specifico che Dognon supporta.

La morale : la programmazione alle interfacce aiuta il nostro codice a comportarsi in modo appropriato e ad essere pronti al cambiamento. Prepara anche il nostro codice a dipendere da astrazioni, il che porta ancora più vantaggi.


1
Supponendo che tu non abbia interfacce eccessivamente ampie, cioè.
Casey,

4

Se sto scrivendo una nuova classe Swimmerper aggiungere la funzionalità swim()e ho bisogno di usare un oggetto di classe dire Dog, e questa Dogclasse implementa l'interfaccia Animalche dichiara swim().

Nella parte superiore della gerarchia ( Animal), è molto astratto mentre nella parte inferiore ( Dog) è molto concreto. Il modo in cui penso alla "programmazione alle interfacce" è che, mentre scrivo la Swimmerclasse, voglio scrivere il mio codice sull'interfaccia che è fino a quella gerarchia che in questo caso è un Animaloggetto. Un'interfaccia è priva di dettagli di implementazione e quindi rende il tuo codice liberamente accoppiato.

I dettagli dell'implementazione possono essere modificati con il tempo, tuttavia, non influirebbe sul codice rimanente poiché tutto ciò con cui interagisci è con l'interfaccia e non con l'implementazione. Non ti interessa come sia l'implementazione ... tutto quello che sai è che ci sarà una classe che implementerà l'interfaccia.


3

Quindi, solo per farlo bene, il vantaggio di un'interfaccia è che posso separare la chiamata di un metodo da una particolare classe. Invece creando un'istanza dell'interfaccia, in cui l'implementazione è data da qualunque classe scelgo che implementa quell'interfaccia. Consentendomi così di avere molte classi, che hanno funzionalità simili ma leggermente diverse e in alcuni casi (i casi relativi all'intenzione dell'interfaccia) non importa quale oggetto sia.

Ad esempio, potrei avere un'interfaccia di movimento. Un metodo che fa "muovere" qualcosa e qualsiasi oggetto (Persona, Macchina, Gatto) che implementa l'interfaccia di movimento potrebbe essere trasmesso e detto di muoversi. Senza il metodo tutti conoscono il tipo di classe che è.


3

Immagina di avere un prodotto chiamato "Zebra" che può essere esteso dai plugin. Trova i plugin cercando DLL in alcune directory. Carica tutte quelle DLL e utilizza la riflessione per trovare le classi che implementano IZebraPlugin, quindi chiama i metodi di tale interfaccia per comunicare con i plugin.

Questo lo rende completamente indipendente da qualsiasi specifica classe di plugin - non importa quali siano le classi. Si preoccupa solo che soddisfino le specifiche dell'interfaccia.

Le interfacce sono un modo per definire punti di estensibilità come questo. Il codice che comunica con un'interfaccia è accoppiato più liberamente - in realtà non è affatto associato a nessun altro codice specifico. Può interagire con plugin scritti anni dopo da persone che non hanno mai incontrato lo sviluppatore originale.

È possibile invece utilizzare una classe base con funzioni virtuali: tutti i plugin verrebbero derivati ​​dalla classe base. Ma questo è molto più limitante perché una classe può avere solo una classe base, mentre può implementare un numero qualsiasi di interfacce.


3

Spiegazione C ++.

Pensa a un'interfaccia come ai metodi pubblici delle tue classi.

È quindi possibile creare un modello che "dipende" da questi metodi pubblici al fine di svolgere la propria funzione (rende le chiamate di funzione definite nell'interfaccia pubblica delle classi). Supponiamo che questo modello sia un contenitore, come una classe Vector, e l'interfaccia da cui dipende è un algoritmo di ricerca.

Qualsiasi classe di algoritmo che definisce le funzioni / l'interfaccia di Vector che effettua le chiamate soddisferà il "contratto" (come qualcuno ha spiegato nella risposta originale). Gli algoritmi non devono nemmeno essere della stessa classe base; l'unico requisito è che le funzioni / i metodi da cui Vector dipende (interfaccia) sia definito nel tuo algoritmo.

Il punto di tutto ciò è che potresti fornire qualsiasi algoritmo / classe di ricerca diversi purché fornisca l'interfaccia da cui Vector dipende (ricerca bolle, ricerca sequenziale, ricerca rapida).

Potresti anche voler progettare altri contenitori (elenchi, code) che sfrutteranno lo stesso algoritmo di ricerca di Vector facendo in modo che soddisfino l'interfaccia / contratto da cui dipendono gli algoritmi di ricerca.

Ciò consente di risparmiare tempo (principio OOP "riutilizzo del codice") in quanto è possibile scrivere un algoritmo una volta invece che ancora e ancora e ancora una volta specifico per ogni nuovo oggetto creato senza complicare eccessivamente il problema con un albero di eredità troppo cresciuto.

Quanto a "perdere" su come funzionano le cose; big-time (almeno in C ++), poiché è così che funziona la maggior parte del framework della libreria TEMPLATE standard.

Ovviamente, quando si usano l'ereditarietà e le classi astratte, la metodologia di programmazione a un'interfaccia cambia; ma il principio è lo stesso, le tue funzioni / metodi pubblici sono l'interfaccia delle tue classi.

Questo è un argomento enorme e uno dei principi cardine di Design Patterns.


3

In Java queste classi concrete implementano tutte l'interfaccia CharSequence:

CharBuffer, String, StringBuffer, StringBuilder

Queste classi concrete non hanno una classe genitore comune diversa dall'Oggetto, quindi non c'è nulla che le colleghi, a parte il fatto che ognuna ha a che fare con array di caratteri, che rappresentano tali o manipolano tali. Ad esempio, i caratteri di String non possono essere modificati una volta creata un'istanza di un oggetto String, mentre è possibile modificare i caratteri di StringBuffer o StringBuilder.

Tuttavia, ognuna di queste classi è in grado di implementare adeguatamente i metodi dell'interfaccia CharSequence:

char charAt(int index)
int length()
CharSequence subSequence(int start, int end)
String toString()

In alcuni casi, le classi della libreria di classi Java che erano solite accettare String sono state riviste per accettare l'interfaccia CharSequence. Quindi se hai un'istanza di StringBuilder, invece di estrarre un oggetto String (il che significa creare un'istanza di una nuova istanza di oggetto), può invece semplicemente passare lo stesso StringBuilder mentre implementa l'interfaccia CharSequence.

L'interfaccia appendibile implementata da alcune classi ha lo stesso tipo di vantaggio per qualsiasi situazione in cui i caratteri possono essere aggiunti a un'istanza dell'istanza dell'oggetto della classe concreta sottostante. Tutte queste classi concrete implementano l'interfaccia Appendable:

BufferedWriter, CharArrayWriter, CharBuffer, FileWriter, FilterWriter, LogStream, OutputStreamWriter, PipedWriter, PrintStream, PrintWriter, StringBuffer, StringBuilder, StringWriter, Writer


È un peccato che le interfacce CharSequencesiano così anemiche. Vorrei che Java e .NET avessero permesso alle interfacce di avere un'implementazione di default, in modo che le persone non abbattessero le interfacce esclusivamente allo scopo di ridurre al minimo il codice del boilerplate. Data qualsiasi CharSequenceimplementazione legittima, si potrebbe emulare la maggior parte delle funzioni Stringdell'uso dei soli quattro metodi precedenti, ma molte implementazioni potrebbero svolgere tali funzioni in modo molto più efficiente in altri modi. Sfortunatamente, anche se una particolare implementazione di CharSequencecontiene tutto in un singolo char[]e potrebbe eseguire molti ...
supercat

... operazioni come la indexOfrapida, non c'è modo che un chiamante che non abbia familiarità con una particolare implementazione CharSequencepossa chiedergli di farlo piuttosto che doverlo usare charAtper esaminare ogni singolo personaggio.
supercat,

3

Racconto: a un postino viene chiesto di tornare a casa dopo casa e di ricevere le copertine contenenti (lettere, documenti, assegni, carte regalo, domanda, lettera d'amore) con l'indirizzo scritto su di esso da consegnare.

Supponiamo che non ci sia copertura e chieda al postino di tornare a casa dopo casa e ricevere tutte le cose e consegnare ad altre persone, il postino può confondersi.

Quindi meglio avvolgerlo con la copertina (nella nostra storia è l'interfaccia) quindi farà bene il suo lavoro.

Ora il compito del postino è ricevere e consegnare solo le copertine (non si preoccuperebbe di cosa ci sia dentro la copertina).

Crea un tipo di interface non reale, ma implementalo con il tipo effettivo.

Creare un'interfaccia significa ottenere i componenti adattano facilmente al resto del codice

Ti faccio un esempio.

hai l'interfaccia AirPlane come di seguito.

interface Airplane{
    parkPlane();
    servicePlane();
}

Supponiamo di avere metodi simili nella tua classe di piani Controller

parkPlane(Airplane plane)

e

servicePlane(Airplane plane)

implementato nel tuo programma. Non interromperà il tuo codice. Voglio dire, non deve cambiare finché accetta argomenti comeAirPlane .

Perché sarà accettare qualsiasi aeroplano nonostante tipo effettivo, flyer, highflyr,fighter , etc.

Inoltre, in una raccolta:

List<Airplane> plane; // Prenderà tutti i tuoi aerei.

L'esempio seguente chiarirà la tua comprensione.


Hai un aereo da combattimento che lo implementa, quindi

public class Fighter implements Airplane {

    public void  parkPlane(){
        // Specific implementations for fighter plane to park
    }
    public void  servicePlane(){
        // Specific implementatoins for fighter plane to service.
    }
}

La stessa cosa per HighFlyer e altri clasess:

public class HighFlyer implements Airplane {

    public void  parkPlane(){
        // Specific implementations for HighFlyer plane to park
    }

    public void  servicePlane(){
        // specific implementatoins for HighFlyer plane to service.
    }
}

Ora pensa che le tue classi di controller utilizzino AirPlanepiù volte,

Supponiamo che la tua classe Controller sia ControlPlane come di seguito,

public Class ControlPlane{ 
 AirPlane plane;
 // so much method with AirPlane reference are used here...
}

Qui la magia arriva quando puoi rendere le tue nuove AirPlaneistanze di tipo quante ne vuoi e non stai cambiando il codice di ControlPlaneclasse.

Puoi aggiungere un'istanza ...

JumboJetPlane // implementing AirPlane interface.
AirBus        // implementing AirPlane interface.

È possibile rimuovere anche istanze di tipi precedentemente creati.


2

Un'interfaccia è come un contratto, in cui si desidera che la classe di implementazione attui i metodi scritti nel contratto (interfaccia). Poiché Java non fornisce l'ereditarietà multipla, la "programmazione per l'interfaccia" è un buon modo per ottenere l'ereditarietà multipla.

Se si dispone di una classe A che sta già estendendo un'altra classe B, ma si desidera che anche quella classe A segua determinate linee guida o attui un determinato contratto, è possibile farlo mediante la strategia di "programmazione per interfacciare".


2

D: - ... "Potresti usare una classe che implementa un'interfaccia?"
A: - Sì.

Q: - ... "Quando dovresti farlo?"
A: - Ogni volta che hai bisogno di una o più classi che implementano le interfacce.

Nota: non è stato possibile creare un'istanza di un'interfaccia non implementata da una classe : True.

  • Perché?
  • Perché l'interfaccia ha solo prototipi di metodi, non definizioni (solo nomi di funzioni, non la loro logica)

AnIntf anInst = new Aclass();
// potremmo farlo solo se Aclass implementa AnIntf.
// anInst avrà il riferimento Aclass.


Nota: ora potremmo capire cosa è successo se Bclass e Cclass hanno implementato lo stesso Dintf.

Dintf bInst = new Bclass();  
// now we could call all Dintf functions implemented (defined) in Bclass.

Dintf cInst = new Cclass();  
// now we could call all Dintf functions implemented (defined) in Cclass.

Cosa abbiamo: stessi prototipi di interfaccia (nomi di funzioni nell'interfaccia) e chiamate implementazioni diverse.

Bibliografia: prototipi - wikipedia


1

Il programma su un'interfaccia consente di modificare senza problemi l'implementazione del contratto definito dall'interfaccia. Permette un accoppiamento libero tra contratto e implementazioni specifiche.

IInterface classRef = new ObjectWhatever()

Potresti usare qualsiasi classe che implementa IInterface? Quando dovresti farlo?

Dai un'occhiata a questa domanda SE per un buon esempio.

Perché l'interfaccia per una classe Java dovrebbe essere preferita?

l'utilizzo di un'interfaccia ha colpito le prestazioni?

se cosi, quanto?

Sì. Avrà un leggero sovraccarico di prestazioni in meno di secondi. Ma se l'applicazione richiede di modificare in modo dinamico l'implementazione dell'interfaccia, non preoccuparti dell'impatto sulle prestazioni.

come puoi evitarlo senza dover mantenere due bit di codice?

Non cercare di evitare più implementazioni di interfaccia se l'applicazione ne ha bisogno. In assenza di un accoppiamento stretto dell'interfaccia con un'implementazione specifica, potrebbe essere necessario distribuire la patch per cambiare un'implementazione in un'altra implementazione.

Un buon caso d'uso: implementazione del modello di strategia:

Esempio reale del modello di strategia


1

programma a un'interfaccia è un termine tratto dal libro GOF. non direi direttamente che ha a che fare con l'interfaccia Java ma piuttosto interfacce reali. per ottenere una separazione dei livelli pulita, è necessario creare una separazione tra i sistemi, ad esempio: supponiamo che tu abbia un database concreto che desideri utilizzare, non "programmeresti mai sul database", invece "programmeresti sull'interfaccia di archiviazione". Allo stesso modo non si dovrebbe mai "programmare su un servizio Web", ma piuttosto programmare su una "interfaccia client". questo è così puoi facilmente scambiare le cose.

trovo che queste regole mi aiutino:

1 . usiamo un'interfaccia java quando abbiamo più tipi di un oggetto. se ho un solo oggetto, non vedo il punto. se ci sono almeno due implementazioni concrete di qualche idea, allora userei un'interfaccia java.

2 . se come ho detto sopra, si desidera portare il disaccoppiamento da un sistema esterno (sistema di archiviazione) al proprio sistema (DB locale), utilizzare anche un'interfaccia.

nota come ci sono due modi per considerare quando usarli. spero che sia di aiuto.


0

Inoltre vedo molte risposte valide ed esplicative qui, quindi voglio dare il mio punto di vista qui, includendo alcune informazioni extra che cosa ho notato durante l'utilizzo di questo metodo.

Test unitari

Negli ultimi due anni ho scritto un progetto per hobby e non ho scritto test unitari per questo. Dopo aver scritto circa 50.000 righe ho scoperto che sarebbe davvero necessario scrivere test unitari. Non ho usato interfacce (o molto parsimoniosamente) ... e quando ho fatto il mio primo test unitario, ho scoperto che era complicato. Perché?

Perché ho dovuto creare molte istanze di classe, utilizzate per l'input come variabili di classe e / o parametri. Quindi i test assomigliano più ai test di integrazione (dovendo creare un "quadro" completo di classi poiché tutto era legato insieme).

Paura delle interfacce Così ho deciso di usare le interfacce. La mia paura era che dovevo implementare tutte le funzionalità ovunque (in tutte le classi usate) più volte. In qualche modo questo è vero, tuttavia, usando l'ereditarietà può essere ridotto molto.

Combinazione di interfacce ed ereditarietà Ho scoperto che la combinazione è molto buona da usare. Faccio un esempio molto semplice.

public interface IPricable
{
    int Price { get; }
}

public interface ICar : IPricable

public abstract class Article
{
    public int Price { get { return ... } }
}

public class Car : Article, ICar
{
    // Price does not need to be defined here
}

In questo modo non è necessario copiare il codice, pur avendo il vantaggio di utilizzare un'auto come interfaccia (ICar).


0

Iniziamo con alcune definizioni prima:

Interfaccia n. L'insieme di tutte le firme definite dalle operazioni di un oggetto è chiamato interfaccia all'oggetto

Digitare n. Un'interfaccia particolare

Un semplice esempio di interfaccia come sopra definito sarebbe tutti i metodi dell'oggetto DOP quali query(), commit(), close()ecc, nel suo insieme, non separatamente. Questi metodi, ovvero la sua interfaccia definiscono il set completo di messaggi, richieste che possono essere inviate all'oggetto.

Un tipo come definito sopra è un'interfaccia particolare. Userò l'interfaccia forma truccata per dimostrare: draw(), getArea(), getPerimeter()ecc ..

Se un oggetto è del tipo Database, intendiamo che accetta messaggi / richieste dell'interfaccia del database query(), commit()ecc. Gli oggetti possono essere di molti tipi. Puoi avere un oggetto database del tipo di forma purché implementi la sua interfaccia, nel qual caso si tratterebbe di una sotto-digitazione .

Molti oggetti possono essere di diverse interfacce / tipi e implementare l'interfaccia in modo diverso. Questo ci consente di sostituire gli oggetti, permettendoci di scegliere quale usare. Conosciuto anche come polimorfismo.

Il client sarà a conoscenza solo dell'interfaccia e non dell'implementazione.

Quindi, in sostanza, la programmazione su un'interfaccia implicherebbe la creazione di un qualche tipo di classe astratta, ad esempio solo Shapecon l'interfaccia specificata draw(), ad esempio getCoordinates(), getArea()ecc. E quindi avere diverse classi concrete implementare quelle interfacce come una classe Circle, una classe Square, una classe Triangle. Quindi programma per un'interfaccia non un'implementazione.


0

"Programma per interfacciare" significa non fornire il codice reale nel modo giusto, il che significa che il codice dovrebbe essere esteso senza interrompere la funzionalità precedente. Solo estensioni, non modificando il codice precedente.

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.