Buone o cattive pratiche per mascherare le raccolte Java con nomi di classe significativi?


46

Ultimamente ho preso l'abitudine di "mascherare" le raccolte Java con nomi di classe a misura d'uomo. Alcuni semplici esempi:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

O:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Un collega mi ha fatto notare che si tratta di una cattiva pratica e introduce ritardo / latenza, oltre ad essere un anti-pattern OO. Posso capirlo introducendo un livello molto piccolo di prestazioni generali, ma non riesco a immaginare che sia del tutto significativo. Quindi chiedo: è buono o cattivo da fare, e perché?


11
È molto più semplice di così. È una cattiva pratica perché immagino che tu stia estendendo le implementazioni della raccolta JDK di base Java. In Java, puoi estendere solo una classe, quindi devi pensare e progettare di più quando hai un'estensione. In Java, usa extension con parsimonia.
Informato il

ChangeListla compilazione si interromperà a extends, poiché Elenco è un'interfaccia, richiede implements. @randomA quello che stai immaginando manca il punto a causa di questo errore
moscerino

@gnat Non manca il punto, presumo che stesse estendendo un implementationie HashMapo TreeMape che cosa avesse un errore di battitura.
Informato il

4
Questa è una cattiva pratica. MALE MALE MALE. Non farlo Tutti sanno cos'è una mappa <String, Widget>. Ma un WidgetCache? Ora devo aprire WidgetCache.java, devo ricordare che WidgetCache è solo una mappa. Devo controllare ogni volta che esce una nuova versione che non hai aggiunto qualcosa di nuovo a WidgetCache. Dio no, non farlo mai.
Miles Rout

"Se vedessi un ArrayList <ArrayList <? >> essere passato in giro nel codice, scapperesti urlando ...??" No, mi sento abbastanza a mio agio con le raccolte generiche nidificate. E lo dovresti essere anche tu.
Kevin Krumwiede,

Risposte:


75

Lag / latenza? Chiamo BS su questo. Dovrebbe esserci esattamente zero overhead da questa pratica. ( Modifica: è stato sottolineato nei commenti che questo può, in effetti, inibire le ottimizzazioni eseguite dalla VM HotSpot. Non conosco abbastanza sull'implementazione della VM per confermarlo o negarlo. Stavo basando il mio commento sul C ++ implementazione di funzioni virtuali.)

C'è del sovraccarico di codice. Devi creare tutti i costruttori dalla classe base che desideri, inoltrandone i parametri.

Inoltre non lo vedo come un anti-pattern, di per sé. Tuttavia, lo vedo come un'occasione mancata. Invece di creare una classe che deriva la classe di base solo per motivi di ridenominazione, che ne pensi di creare una classe che contiene la raccolta e offre un'interfaccia migliorata specifica per ogni caso? La cache dei widget dovrebbe davvero offrire l'interfaccia completa di una mappa? O dovrebbe invece offrire un'interfaccia specializzata?

Inoltre, nel caso delle raccolte, il modello semplicemente non funziona insieme alla regola generale dell'uso delle interfacce, non delle implementazioni - vale a dire, in un semplice codice di raccolta, si dovrebbe creare un HashMap<String, Widget>e quindi assegnarlo a una variabile di tipo Map<String, Widget>. Il tuo WidgetCachenon può estendere Map<String, Widget>, perché è un'interfaccia. Non può essere un'interfaccia che estende l'interfaccia di base, perché HashMap<String, Widget>non implementa quell'interfaccia, e nemmeno un'altra raccolta standard. E mentre puoi renderlo una classe che si estende HashMap<String, Widget>, devi quindi dichiarare le variabili come WidgetCacheo Map<String, Widget>, e il primo ti perde la flessibilità di sostituire una raccolta diversa (forse una raccolta di caricamento lazy di ORM), mentre il secondo tipo di sconfigge il punto di avere la classe.

Alcuni di questi contrappunti si applicano anche alla mia classe specializzata proposta.

Questi sono tutti punti da considerare. Potrebbe essere o meno la scelta giusta. In entrambi i casi, gli argomenti offerti dal collega non sono validi. Se pensa che sia un anti-schema, dovrebbe nominarlo.


1
Nota però che mentre è del tutto possibile avere un certo impatto sulle prestazioni, non sarà così orribile (perdere alcune ottimizzazioni integrate e ottenere una chiamata extra nel peggiore dei casi - non eccezionale nel tuo ciclo più interno ma altrimenti probabilmente va bene).
Voo,

5
Questa risposta davvero buona dice a tutti "non giudicare la decisione in base al sovraccarico prestazionale" e l'unica discussione qui sotto è "come fa HotSpot a gestirlo, c'è qualche (nel 99,9% dei casi) un impatto irrilevante sulle prestazioni" - ragazzi, ti sei mai preso la briga di leggere più della prima frase?
Doc Brown,

16
+1 per "Invece di creare una classe che deriva la classe di base solo per motivi di ridenominazione, che ne dici di creare una classe che contiene la raccolta e offre un'interfaccia migliorata, specifica per ogni caso?"
user11153,

6
Esempio pratico che illustra il punto principale di questa risposta: La documentazione di public class Properties extends Hashtable<Object,Object>in java.utildice "Poiché le proprietà ereditano da Hashtable, i metodi put e putAll possono essere applicati a un oggetto Properties. Il loro uso è fortemente sconsigliato in quanto consentono al chiamante di inserire voci le cui chiavi o i valori non sono stringhe ". La composizione sarebbe stata molto più pulita.
Patricia Shanahan,

3
@DocBrown No, questa risposta afferma che non vi è alcun impatto sulle prestazioni. Se fai affermazioni forti, è meglio che tu sia in grado di supportarle, altrimenti le persone probabilmente ti chiameranno su questo. Il punto centrale dei commenti (sulle risposte) è di evidenziare fatti o ipotesi imprecisi o aggiungere note utili, ma certamente non congratularmi con le persone, ecco a cosa serve il sistema di voto.
Voo,

25

Secondo IBM questo in realtà è un anti-pattern. Queste classi simili a "typedef" sono chiamate tipi psuedo.

L'articolo lo spiega molto meglio di me, ma proverò a riassumerlo nel caso in cui il link non funzioni:

  • Qualsiasi codice che prevede a WidgetCachenon può gestire aMap<String, Widget>
  • Questi pseudotipi sono "virali" quando si usano più pacchetti che portano a incompatibilità mentre il tipo base (solo una stupida Mappa <...>) avrebbe funzionato in tutti i casi in tutti i pacchetti.
  • I pseudo tipi sono spesso concreti, non implementano interfacce specifiche perché le loro classi di base implementano solo la versione generica.

Nell'articolo propongono il seguente trucco per rendere la vita più semplice senza usare pseudo tipi:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Che funziona a causa dell'inferenza di tipo automatica.

(Sono arrivato a questa risposta tramite questa domanda di overflow dello stack correlata)


13
La necessità newHashMapdel diamante non è risolta ora?
svick,

Assolutamente vero, dimenticato. Normalmente non lavoro in Java.
Roy T.

Vorrei che le lingue consentissero un universo di tipi variabili che contenesse riferimenti a istanze di altri tipi, ma che potesse comunque supportare il controllo in fase di compilazione, in modo tale che un valore di tipo WidgetCachepotesse essere assegnato a una variabile di tipo WidgetCacheo Map<String,Widget>senza un cast, ma lì era un mezzo con cui un metodo statico WidgetCachepoteva fare in modo che prendesse un riferimento Map<String,Widget>e lo restituisse come tipo WidgetCachedopo aver eseguito la convalida desiderata. Tale caratteristica potrebbe essere particolarmente utile con i generici, che non dovrebbero essere cancellati dal tipo (dal ...
supercat

... i tipi esisterebbero comunque solo nella mente del compilatore). Per molti tipi mutabili, esistono due tipi comuni di campi di riferimento: uno che contiene l'unico riferimento a un'istanza che può essere mutata o uno che contiene un riferimento condivisibile a un'istanza a cui nessuno è autorizzato a mutare. Sarebbe utile poter assegnare nomi diversi a questi due tipi di campi, poiché richiedono modelli di utilizzo diversi, ma entrambi devono contenere riferimenti agli stessi tipi di istanze di oggetti.
supercat

5
Questo è un ottimo argomento per cui Java ha bisogno di alias di tipo.
GlenPeterson,

13

Il colpo di prestazione sarebbe limitato al massimo a una ricerca di vtable, che molto probabilmente stai già subendo. Non è un motivo valido per opporvisi.

La situazione è abbastanza comune che la maggior parte di tutti i linguaggi di programmazione tipicamente statici hanno una sintassi speciale per i tipi di alias, generalmente chiamati a typedef. Java probabilmente non li ha copiati perché in origine non aveva tipi con parametri. L'estensione di una classe non è l'ideale, a causa dei motivi che Sebastian ha affrontato così bene nella sua risposta, ma può essere una soluzione ragionevole per la sintassi limitata di Java.

I typedef presentano numerosi vantaggi. Esprimono più chiaramente l'intenzione del programmatore, con un nome migliore, a un livello più appropriato di astrazione. Sono più facili da cercare per scopi di debug o refactoring. Considera di trovare ovunque a WidgetCachesia usato anziché trovare quegli usi specifici di a Map. Sono più facili da modificare, ad esempio se in seguito scopri di aver bisogno di un LinkedHashMapcontenitore o anche del tuo contenitore personalizzato.


C'è anche un minimo sovraccarico nei costruttori.
Stephen C,

11

Ti suggerisco, come già menzionato da altri, di usare la composizione sull'ereditarietà, in modo da poter esporre solo i metodi realmente necessari, con nomi che corrispondono al caso d'uso previsto. Gli utenti della tua classe devono davvero sapere che si WidgetCachetratta di una mappa? Ed essere in grado di farne tutto ciò che vogliono? O hanno solo bisogno di sapere che è una cache per i widget?

Classe di esempio dal mio codebase, con soluzione a un problema simile che hai descritto:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Puoi vedere che internamente è "solo una mappa", ma l'interfaccia pubblica non lo mostra. E ha metodi "a misura di programmatore" come appendMessage(Locale locale, String code, String message)per un modo più semplice e significativo di inserire nuove voci. E gli utenti di classe non possono farlo, ad esempio translations.clear(), perché Translationsnon si stanno estendendo Map.

Facoltativamente, puoi sempre delegare alcuni dei metodi necessari alla mappa utilizzata internamente.


6

Vedo questo come un esempio di un'astrazione significativa. Una buona astrazione ha un paio di tratti:

  1. Nasconde i dettagli di implementazione che sono irrilevanti per il codice che lo consuma.

  2. È tanto complesso quanto deve essere.

Estendendo, stai esponendo l'intera interfaccia del genitore, ma in molti casi potrebbe essere meglio nascosto, quindi vorrai fare ciò che Sebastian Redl suggerisce e favorire la composizione rispetto all'eredità e aggiungere un'istanza del genitore come un membro privato della tua classe personalizzata. Qualsiasi metodo di interfaccia che abbia senso per la tua astrazione può essere facilmente delegato (nel tuo caso) alla collezione interna.

Per quanto riguarda l'impatto sulle prestazioni, è sempre una buona idea ottimizzare prima il codice per la leggibilità e, se si sospetta un impatto sulle prestazioni, profilare il codice per confrontare le due implementazioni.


4

+1 alle altre risposte qui. Aggiungerò anche che in realtà è considerata un'ottima pratica dalla community di Domain Driven Design (DDD). Sostengono che il tuo dominio e le interazioni con esso debbano avere un significato semantico rispetto alla struttura di dati sottostante. A Map<String, Widget>potrebbe essere una cache, ma potrebbe anche essere qualcos'altro, ciò che hai fatto correttamente in My Not So Humble Opinion (IMNSHO) è modellare ciò che la raccolta rappresenta, in questo caso una cache.

Aggiungerò un'importante modifica in quanto il wrapper della classe di dominio attorno alla struttura di dati sottostante dovrebbe probabilmente avere anche altre variabili o funzioni del membro che lo rendono davvero una classe di dominio con interazioni anziché solo una struttura di dati (se solo Java avesse Tipi di valore , li avremo in Java 10 - prometto!)

Sarà interessante vedere quale impatto avranno i flussi di Java 8 su tutto questo, posso immaginare che forse alcune interfacce pubbliche preferiranno gestire un flusso di (inserire una primitiva o stringa Java comune) anziché un oggetto Java.

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.