Ha sempre senso "programmare su un'interfaccia" in Java?


9

Ho visto la discussione su questa domanda su come sarebbe istanziata una classe che implementa da un'interfaccia. Nel mio caso, sto scrivendo un programma molto piccolo in Java che utilizza un'istanza di TreeMap, e secondo l'opinione di tutti lì, dovrebbe essere istanziato come:

Map<X> map = new TreeMap<X>();

Nel mio programma, sto chiamando la funzione map.pollFirstEntry(), che non è dichiarata Mapnell'interfaccia (e un paio di altri che sono presenti Mapanche nell'interfaccia). Sono riuscito a farlo eseguendo il casting in un TreeMap<X>ovunque io chiamo questo metodo come:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

Comprendo i vantaggi delle linee guida di inizializzazione descritte sopra per i programmi di grandi dimensioni, tuttavia per un programma molto piccolo in cui questo oggetto non sarebbe passato ad altri metodi, riterrei che non sia necessario. Tuttavia, sto scrivendo questo codice di esempio come parte di una domanda di lavoro e non voglio che il mio codice appaia male o ingombra. Quale sarebbe la soluzione più elegante?

EDIT: Vorrei sottolineare che sono più interessato alle buone pratiche di codifica piuttosto che all'applicazione della funzione specifica TreeMap. Come alcune delle risposte hanno già sottolineato (e ho contrassegnato come risposta la prima per farlo), è necessario utilizzare il livello di astrazione più elevato possibile, senza perdere funzionalità.


1
Utilizzare una TreeMap quando sono necessarie le funzionalità. Probabilmente è stata una scelta progettuale per un motivo specifico, quindi dovrebbe essere parte dell'attuazione.

@JordanReiter dovrei solo aggiungere una nuova domanda con lo stesso contenuto o esiste un meccanismo interno di post-pubblicazione?
jimijazz,


2
"Comprendo i vantaggi delle linee guida di inizializzazione descritte sopra per programmi di grandi dimensioni" Dover eseguire il casting ovunque non è vantaggioso, indipendentemente dalle dimensioni del programma
Ben Aaronson,

Risposte:


23

"Programmare su un'interfaccia" non significa "utilizzare la versione più astratta possibile". In quel caso tutti lo userebbero Object.

Ciò significa che dovresti definire il tuo programma in base alla minima astrazione possibile senza perdere funzionalità . Se hai bisogno di un TreeMapallora dovrai definire un contratto usando un TreeMap.


2
TreeMap non è un'interfaccia, è una classe di implementazione. Implementa le interfacce Map, SortedMap e NavigableMap. Il metodo descritto fa parte dell'interfaccia NavigableMap . L'uso di TreeMap impedisce all'implementazione di passare a ConcurrentSkipListMap (ad esempio) che è l'intero punto di codifica di un'interfaccia piuttosto che dell'implementazione.

3
@MichaelT: Non ho guardato l'esatta astrazione di cui ha bisogno in questo specifico scenario, quindi ho usato TreeMapcome esempio. Il "programma per un'interfaccia" non dovrebbe essere considerato come un'interfaccia o una classe astratta: un'implementazione può anche essere considerata un'interfaccia.
Jeroen Vannevel,

1
Anche se l'interfaccia / i metodi pubblici di una classe di implementazione sono tecnicamente una "interfaccia", infrangono il concetto alla base di LSP e impediscono la sostituzione di una sottoclasse diversa, motivo per cui si desidera programmare un " public interfacepiuttosto che i metodi pubblici di un'implementazione" .

@JeroenVannevel Concordo sul fatto che la programmazione su un'interfaccia può essere eseguita quando l' interfaccia è effettivamente rappresentata da una classe. Tuttavia, non vedo quale beneficio TreeMapavrebbe avuto su SortedMapoNavigableMap
toniedzwiedz il

16

Se vuoi ancora usare un'interfaccia che potresti usare

NavigableMap <X, Y> map = new TreeMap<X, Y>();

non è necessario utilizzare sempre un'interfaccia, ma spesso esiste un punto in cui si desidera avere una visione più generale che consente di sostituire l'implementazione (forse per il test) e questo è facile se tutti i riferimenti all'oggetto sono astratti come tipo di interfaccia.


3

Il punto alla base della programmazione di un'interfaccia piuttosto che di un'implementazione è quello di evitare la perdita di dettagli di implementazione che altrimenti limiterebbero il programma.

Considera la situazione in cui la versione originale del tuo codice ha usato HashMaped esposto quella.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Ciò significa che qualsiasi modifica a getFoo()è una rottura dell'API e renderebbe infelici le persone che la utilizzano. Se tutto ciò che stai garantendo è foouna mappa, devi invece restituirla.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Questo ti dà la flessibilità di cambiare il modo in cui le cose funzionano all'interno del tuo codice. Ti rendi conto che in foorealtà deve essere una mappa che restituisce le cose in un ordine particolare.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

E questo non rompe nulla per il resto del codice.

Successivamente puoi rafforzare il contratto che la classe dà senza rompere nulla.

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Questo approfondisce il principio di sostituzione di Liskov

La sostituibilità è un principio nella programmazione orientata agli oggetti. Afferma che, in un programma per computer, se S è un sottotipo di T, allora gli oggetti di tipo T possono essere sostituiti con oggetti di tipo S (ovvero, oggetti di tipo S possono sostituire oggetti di tipo T) senza alterare nessuno dei desiderabili proprietà di quel programma (correttezza, compito svolto, ecc.).

Poiché NavigableMap è un sottotipo di Mappa, questa sostituzione può essere effettuata senza alterare il programma.

Esporre i tipi di implementazione rende difficile cambiare il modo in cui il programma funziona internamente quando è necessario apportare una modifica. Questo è un processo doloroso e molte volte crea brutte soluzioni che servono solo a infliggere più dolore al programmatore in seguito (ti sto guardando un programmatore precedente che ha continuato a mescolare i dati tra una LinkedHashMap e una TreeMap per qualche motivo - fidati di me, ogni volta che vedi il tuo nome in svn biasimo, mi preoccupo).

Si vorrebbe comunque evitare perdite di tipi di implementazione. Ad esempio, potresti voler implementare ConcurrentSkipListMap invece a causa di alcune caratteristiche prestazionali o ti piace semplicemente java.util.concurrent.ConcurrentSkipListMappiuttosto che java.util.TreeMapnelle dichiarazioni di importazione o altro.


1

Concordando con le altre risposte che dovresti usare la classe (o l'interfaccia) più generica di cui hai effettivamente bisogno, in questo caso TreeMap (o come qualcuno ha suggerito NavigableMap). Ma vorrei aggiungere che questo è sicuramente meglio che lanciarvi ovunque, il che sarebbe comunque un odore molto più grande. Vedi /programming/4167304/why-should-casting-be-avoided per alcuni motivi.


1

Si tratta di comunicare le tue intenzioni su come usare l'oggetto. Ad esempio, se il metodo prevede un Mapoggetto con un ordine di iterazione prevedibile :

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

Quindi, se hai assolutamente bisogno di dire ai chiamanti del metodo sopra che anche esso restituisce un Mapoggetto con un ordine di iterazione prevedibile , perché c'è qualche aspettativa per qualche motivo:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Naturalmente, i chiamanti possono comunque considerare l'oggetto di ritorno Mapcome tale, ma questo va oltre lo scopo del metodo:

private Map<String, String> output = processOrderedMap(input);

Facendo un passo indietro

Il consiglio generale di codifica per un'interfaccia è (generalmente) applicabile, perché di solito è l'interfaccia che fornisce la garanzia su ciò che l'oggetto dovrebbe essere in grado di eseguire, ovvero il contratto . Molti principianti iniziano con HashMap<K, V> map = new HashMap<>()e sono invitati a dichiararlo come a Map, perché a HashMapnon offre nulla di più di quello che Mapdovrebbe fare. Da questo, saranno quindi in grado di capire (si spera) perché i loro metodi dovrebbero includere in Mapinvece di a HashMap, e questo consente loro di realizzare la funzione di ereditarietà in OOP.

Per citare solo una riga dalla voce di Wikipedia del principio preferito da tutti in relazione a questo argomento:

È una relazione semantica piuttosto che semplicemente sintattica perché intende garantire l'interoperabilità semantica dei tipi in una gerarchia ...

In altre parole, usare una Mapdichiarazione non è perché ha senso "sintatticamente", ma piuttosto le invocazioni sull'oggetto dovrebbero preoccuparsi solo che sia un tipo di Map.

Codice più pulito

Trovo che questo mi permetta di scrivere anche un codice più pulito a volte, specialmente quando si tratta di test unitari. La creazione di un HashMapcon una sola voce di test di sola lettura richiede più di una riga (escluso l'uso dell'inizializzazione a doppia parentesi), quando posso facilmente sostituirla con Collections.singletonMap().

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.