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

9

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

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

Nel mio programma, chiamo la funzione map.pollFirstEntry() , che non è dichiarata nell'interfaccia Map (e un altro paio che sono presenti anche nell'interfaccia Map ). Sono riuscito a farlo gettando a 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, penserei che non sia necessario. Tuttavia, sto scrivendo questo codice di esempio come parte di un'applicazione di lavoro, e non voglio che il mio codice appaia male né 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à indicato (e ho contrassegnato come risposta il primo a farlo), dovrebbe essere usato il livello di astrazione più alto possibile, senza perdere funzionalità.

    
posta jimijazz 08.05.2015 - 14:53
fonte

5 risposte

23

"Programmare su un'interfaccia" non significa "usare la versione più astratta possibile". In quel caso tutti useranno solo Object .

Significa che dovresti definire il tuo programma contro l'astrazione più bassa possibile senza perdere la funzionalità . Se richiedi un TreeMap , dovrai definire un contratto utilizzando TreeMap .

    
risposta data 08.05.2015 - 14:57
fonte
16

Se vuoi ancora utilizzare un'interfaccia, puoi usare

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

non è necessario usare sempre un'interfaccia ma spesso c'è un punto in cui si vuole prendere una visione più generale che consente di sostituire l'implementazione (forse per il test) e questo è facile se tutti i riferimenti all'oggetto sono astratto come il tipo di interfaccia.

    
risposta data 08.05.2015 - 15:02
fonte
3

Il punto dietro la codifica di un'interfaccia piuttosto che un'implementazione è di evitare la perdita di dettagli di implementazione che altrimenti vincolerebbero il tuo programma.

Considera la situazione in cui la versione originale del tuo codice ha utilizzato HashMap ed è stato esposto.

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 modifica dell'API di rottura e renderebbe le persone che lo utilizzano infelice. Se tutto ciò che stai garantendo è che foo è una mappa, dovresti 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 foo in realtà 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 ciò non infrange nulla per il resto del codice.

In seguito 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

Substitutability is a principle in object-oriented programming. It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).

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

L'esposizione dei tipi di implementazione rende difficile modificare il modo in cui il programma funziona internamente quando è necessario apportare una modifica. Questo è un processo doloroso e molte volte crea brutti stratagemmi che servono solo a infliggere più dolore al coder in un secondo momento (sto guardando a te il coder precedente che continuava a mescolare dati tra una LinkedHashMap e una TreeMap per qualche motivo - fidati di me, ogni volta che vedi il tuo nome in svn colpa mi preoccupo)

Dovresti ancora voler evitare perdite di tipi di implementazione. Ad esempio, potresti voler implementare ConcurrentSkipListMap invece per alcune caratteristiche di rendimento o ti piace solo java.util.concurrent.ConcurrentSkipListMap piuttosto che java.util.TreeMap nelle istruzioni di importazione o simili.

    
risposta data 13.05.2015 - 18:18
fonte
1

Accettando con le altre risposte dovresti usare la classe più generica (o l'interfaccia) di cui hai effettivamente bisogno, in questo caso TreeMap (o come qualcuno ha suggerito NavigableMap). Ma vorrei aggiungere che questo è sicuramente meglio che lanciarlo ovunque, il che sarebbe comunque un odore molto più grande. Vedi link per alcuni motivi.

    
risposta data 08.05.2015 - 14:58
fonte
1

Riguarda la comunicazione del tuo intento di come deve essere usato l'oggetto. Ad esempio, se il tuo metodo prevede un oggetto Map con un ordine di iterazione prevedibile :

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

Quindi, se è assolutamente necessario indicare ai chiamanti il metodo sopra riportato che restituisce anche un oggetto Map con un ordine di iterazione prevedibile , perché c'è una tale aspettativa per qualche motivo:

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

Naturalmente, i chiamanti possono ancora considerare l'oggetto di ritorno come Map in quanto tale, ma questo va oltre lo scopo del tuo metodo:

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

Fare un passo indietro

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

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

It is a semantic rather than merely syntactic relation because it intends to guarantee semantic interoperability of types in a hierarchy...

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

Codice pulitore

Trovo che ciò mi permetta di scrivere codice più pulito anche a volte, specialmente quando si tratta di test delle unità. La creazione di un HashMap con una singola voce di test di sola lettura richiede più di una riga (escluso l'utilizzo dell'inizializzazione in doppia coppia), quando posso facilmente sostituirlo con Collections.singletonMap() .

    
risposta data 26.05.2015 - 03:58
fonte

Leggi altre domande sui tag