È una cattiva abitudine usare (oltre) la riflessione?


16

È buona norma utilizzare la riflessione se riduce notevolmente la quantità di codice del boilerplate?

Fondamentalmente c'è un compromesso tra prestazioni e forse leggibilità da un lato e astrazione / automazione / riduzione del codice della piastra di caldaia dall'altro lato.

Modifica: ecco un esempio dell'uso raccomandato della riflessione .

Per fare un esempio, supponiamo che esista una classe astratta Baseche ha 10 campi e ha 3 sottoclassi SubclassA, SubclassBe SubclassCognuna con 10 campi diversi; sono tutti fagioli semplici. Il problema è che si ottengono due Baseriferimenti di tipo e si desidera vedere se gli oggetti corrispondenti sono dello stesso (sotto) tipo e sono uguali.

Come soluzioni esiste la soluzione grezza in cui si controlla prima se i tipi sono uguali e quindi si controllano tutti i campi oppure è possibile utilizzare la riflessione e vedere dinamicamente se sono dello stesso tipo e scorrere tutti i metodi che iniziano con "get" (convenzione sopra la configurazione), chiamali su entrambi gli oggetti e chiama uguale ai risultati.

boolean compare(Base base1, Base, base2) {
    if (base1 instanceof SubclassA && base2 instanceof SubclassA) { 
         SubclassA subclassA1 = (SubclassA) base1;
         SubclassA subclassA2 = (SubclassA) base2;
         compare(subclassA1, subclassA2);
    } else if (base1 instanceof SubclassB && base2 instanceof SubclassB) {
         //the same
    }
    //boilerplate
}

boolean compare(SubclassA subA1, SubclassA subA2) {
    if (!subA1.getField1().equals(subA2.getField1)) {
         return false;
    }
    if (!subA1.getField2().equals(subA2.getField2)) {
         return false;
    }
    //boilerplate
}

boolean compare(SubclassB subB1, SubclassB subB2) {
    //boilerplate
}

//boilerplate

//alternative with reflection 
boolean compare(Base base1, Base base2) {
        if (!base1.getClass().isAssignableFrom(base2.getClass())) {
            System.out.println("not same");
            System.exit(1);
        }
        Method[] methods = base1.getClass().getMethods();
        boolean isOk = true;
        for (Method method : methods) {
            final String methodName = method.getName();
            if (methodName.startsWith("get")) {
                Object object1 = method.invoke(base1);
                Object object2 = method.invoke(base2);
                if(object1 == null || object2 == null)  {
                    continue;
                }
                if (!object1.equals(object2)) {
                    System.out.println("not equals because " + object1 + " not equal with " + object2);
                    isOk = false;
                }
            }
        }

        if (isOk) {
            System.out.println("is OK");
        }
}

20
L'abuso di qualcosa è una cattiva abitudine.
Tulains Córdova,

1
@ user61852 Giusto, troppa libertà porta alla dittatura. Qualche vecchio greco lo sapeva già.
ott--

6
“Troppa acqua sarebbe male per te. Ovviamente, troppa è esattamente quella quantità che è eccessiva, ecco cosa significa! ”- Stephen Fry
Jon Purdy

4
"Ogni volta che ti ritrovi a scrivere il codice del modulo" se l'oggetto è di tipo T1, quindi fai qualcosa, ma se è di tipo T2, quindi fai qualcos'altro ", schiaffeggia. Javapractices.com/topic/TopicAction.do?Id = 31
rm5248

Risposte:


25

Reflection è stato creato per uno scopo specifico, di scoprire le funzionalità di una classe che era sconosciuto al momento della compilazione, simile a quello che l' dlopene dlsymfunzioni di fare in C. Ogni uso al di fuori di che dovrebbero essere pesantemente esaminati.

Ti è mai venuto in mente che i progettisti Java stessi hanno riscontrato questo problema? Ecco perché praticamente ogni classe ha un equalsmetodo. Classi diverse hanno definizioni diverse di uguaglianza. In alcune circostanze un oggetto derivato potrebbe essere uguale a un oggetto base. In alcune circostanze, l'uguaglianza potrebbe essere determinata sulla base di campi privati ​​senza getter. Non lo sai.

Ecco perché ogni oggetto che desidera l'uguaglianza personalizzata dovrebbe implementare un equalsmetodo. Alla fine, vorrai mettere gli oggetti in un set o usarli come indice di hash, quindi dovrai implementarli equalscomunque. Altre lingue lo fanno diversamente, ma Java lo usa equals. Dovresti attenersi alle convenzioni della tua lingua.

Inoltre, il codice "boilerplate", se inserito nella classe corretta, è piuttosto difficile da rovinare. Reflection aggiunge ulteriore complessità, il che significa ulteriori possibilità di bug. Nel tuo metodo, ad esempio, due oggetti sono considerati uguali se uno ritorna nullper un determinato campo e l'altro no. Cosa succede se uno dei tuoi getter restituisce uno dei tuoi oggetti, senza un appropriato equals? Il tuo if (!object1.equals(object2))fallirà. Anche renderlo soggetto a bug è il fatto che la riflessione viene usata raramente, quindi i programmatori non hanno familiarità con i suoi gotcha.


12

L'abuso della riflessione probabilmente dipende dal linguaggio usato. Qui stai usando Java. In tal caso, la riflessione dovrebbe essere usata con cura perché spesso è solo una soluzione alternativa per un cattivo design.

Quindi, stai confrontando diverse classi, questo è un problema perfetto per l' override del metodo . Si noti che le istanze di due classi diverse non devono mai essere considerate uguali. Puoi confrontare per uguaglianza solo se hai istanze della stessa classe. Vedi /programming/27581/overriding-equals-and-hashcode-in-java per un esempio su come implementare correttamente il confronto di uguaglianza.


16
+1 - Alcuni di noi credono che QUALSIASI uso della riflessione sia una bandiera rossa che indica un cattivo design.
Ross Patterson,

1
Molto probabilmente in questo caso è auspicabile una soluzione basata su uguali, ma come idea generale cosa c'è di sbagliato nella soluzione di riflessione? In realtà è molto generico e non è necessario che i metodi uguali siano scritti esplicitamente in ogni classe e sottoclasse (sebbene possano essere facilmente generati da un buon IDE).
m3th0dman

2
@RossPatterson Perché?
m3th0dman

2
@ m3th0dman Perché porta a cose come il tuo compare()metodo supponendo che qualsiasi metodo che inizia con "get" sia un getter ed è sicuro e appropriato chiamare come parte di un'operazione di confronto. È una violazione della definizione dell'interfaccia prevista dell'oggetto e, sebbene ciò possa essere utile, è quasi sempre sbagliato.
Ross Patterson,

4
@ m3th0dman Viola l'incapsulamento: la classe base deve accedere agli attributi nelle sue sottoclassi. E se fossero privati ​​(o meglio privati)? Che dire del polimorfismo? Se decido di aggiungere un'altra sottoclasse e voglio fare un confronto diverso in essa? Bene, la classe base lo sta già facendo per me e non posso cambiarlo. E se i getter caricassero qualcosa di pigro? Voglio che il metodo di confronto lo faccia? Come faccio a sapere se un metodo che inizia con getè un getter e non un metodo personalizzato che restituisce qualcosa?
Sulthan,

1

Penso che tu abbia due problemi qui.

  1. Quanto codice dinamico vs statico dovrei avere?
  2. Come esprimo una versione personalizzata dell'uguaglianza?

Codice dinamico vs statico

Questa è una domanda perenne e la risposta è molto supponente.

Da un lato il compilatore è molto bravo a catturare ogni sorta di codice errato. Lo fa attraverso varie forme di analisi, l'analisi del tipo è comune. Sa che non puoi usare un Bananaoggetto nel codice che si aspetta a Cog. Ti dice questo attraverso un errore di compilazione.

Ora può farlo solo quando può inferire sia il Tipo accettato, sia il Tipo dato dal contesto. Quanto si può dedurre e quanto generale sia questa inferenza, dipende in gran parte dalla lingua utilizzata. Java può dedurre informazioni sul tipo attraverso meccanismi come Ereditarietà, Interfacce e Generici. Il chilometraggio varia, alcune altre lingue forniscono meno meccanismi e altre ne forniscono di più. Si riduce ancora a ciò che il compilatore può sapere per essere vero.

D'altra parte il compilatore non può prevedere la forma del codice esterno e talvolta un algoritmo generale può essere espresso su molti tipi che non possono essere facilmente espressi usando il sistema di tipi di linguaggio. In questi casi il compilatore non può sempre conoscere in anticipo il risultato e potrebbe anche non essere in grado di sapere quale domanda porre. Reflection, interfaces e la classe Object sono il modo Java di gestire questi problemi. Dovrai fornire i controlli e la gestione corretti, ma non è dannoso avere questo tipo di codice.

Se rendere il tuo codice molto specifico o molto generico dipende dal problema che stai cercando di gestire. Se potessi esprimerlo facilmente usando il sistema dei tipi, fallo. Lascia che il compilatore giochi ai suoi punti di forza e ti aiuti. Se il sistema di tipi non è stato in grado di conoscere in anticipo (codice esterno) o il sistema di tipi non è adatto per un'implementazione generale del tuo algoritmo, la riflessione (e altri mezzi dinamici) sono lo strumento giusto da usare.

Basta essere consapevoli del fatto che uscire dal sistema dei tipi della tua lingua è sorprendente. Immagina di avvicinarti al tuo amico e iniziare una conversazione in inglese. All'improvviso lascia cadere alcune parole dallo spagnolo, dal francese e dal cantonese per esprimere con precisione i tuoi pensieri. Il contesto dirà molto al tuo amico, ma potrebbe anche non sapere bene come gestire quelle parole che portano a tutti i tipi di equivoci. Affrontare questi equivoci è meglio che spiegare quelle idee in inglese usando più parole?

Uguaglianza personalizzata

Mentre capisco che Java si basa fortemente sul equalsmetodo per confrontare due oggetti in generale, non è sempre adatto in un determinato contesto.

C'è un altro modo, ed è anche uno standard Java. Si chiama Comparatore .

Il modo in cui implementate il vostro comparatore dipenderà da cosa state confrontando e da come.

  • Può essere applicato a due oggetti qualsiasi indipendentemente dalla loro specifica equalsimplementazione.
  • Potrebbe implementare un metodo di confronto generale (basato sulla riflessione) per gestire due oggetti qualsiasi.
  • Le funzioni di confronto delle piastre di caldaia potrebbero essere aggiunte per tipi di oggetti comunemente confrontati, garantendo la sicurezza e l'ottimizzazione dei tipi.

1

Preferisco evitare la programmazione riflessiva il più possibile perché

  • rende il codice più difficile da controllare staticamente dal compilatore
  • rende il codice più difficile da ragionare
  • rende più difficile il refactoring del codice

È anche molto meno performante delle semplici chiamate di metodo; era più lento di un ordine di grandezza o più.

Codice di controllo statico

Qualsiasi codice riflettente cerca classi e metodi usando le stringhe; nell'esempio originale sta cercando qualsiasi metodo che inizi con "get"; restituirà getter ma anche altri metodi, come "gettysburgAddress ()". Le regole potrebbero essere rafforzate nel codice, ma resta il punto che si tratta di un controllo di runtime ; un IDE e il compilatore non possono essere d'aiuto. In generale non mi piace il codice "tipicamente stringa" o "ossessivo primitivo".

Più difficile da ragionare

Il codice riflettente è più dettagliato delle semplici chiamate di metodo. Più codice = più bug, o almeno più potenziale per i bug, più codice da leggere, testare, ecc. Meno è di più.

Più difficile da refactor

Perché il codice è basato su stringhe / dinamicamente; non appena la riflessione è sulla scena, non è possibile eseguire il refactoring del codice con la sicurezza del 100% utilizzando gli strumenti di refactoring di un IDE poiché l'IDE non è in grado di rilevare gli usi riflessivi.

Fondamentalmente, se possibile, evitare la riflessione nel codice generale; cercare un design migliorato.


0

L'abuso di qualcosa per definizione è male, giusto? Quindi liberiamoci del (finito) per ora.

Definiresti una cattiva abitudine l'uso pesantemente interno della primavera della riflessione?

La primavera doma il riflesso usando le annotazioni - così fa Hibernate (e probabilmente dozzine / centinaia di altri strumenti).

Segui questi schemi se lo usi nel tuo codice. Usa le annotazioni per assicurarti che l'IDE del tuo utente possa ancora aiutarle (Anche se sei l'unico "Utente" del tuo codice, l'uso incurante della riflessione probabilmente tornerà a morderti nel culo alla fine).

Tuttavia, senza considerare il modo in cui il codice verrà utilizzato dagli sviluppatori, anche l'uso più semplice di reflection è probabilmente un uso eccessivo.


0

Penso che la maggior parte di queste risposte manchi il punto.

  1. Sì, probabilmente dovresti scrivere equals()e hashcode(), come notato da @KarlBielefeldt.

  2. Ma, per una classe con molti campi, questo può essere noioso boilerplate.

  3. Quindi dipende .

    • Se hai bisogno solo di uguali e hashcode raramente , è pragmatico, e probabilmente ok, usare un calcolo di riflessione per scopi generali. Almeno come primo passaggio rapido e sporco.
    • ma se hai bisogno di uguali, ad esempio questi oggetti vengono inseriti in HashTables, quindi le prestazioni saranno un problema, dovresti sicuramente scrivere il codice. sarà molto più veloce.
  4. Un'altra possibilità: se le tue classi hanno davvero così tanti campi che scrivere un uguale è noioso, considera

    • mettendo i campi in una mappa.
    • puoi ancora scrivere getter e setter personalizzati
    • utilizzare Map.equals()per il confronto. (o Map.hashcode())

es. ( nota : ignorando i controlli null, probabilmente dovrebbe usare Enums invece delle chiavi String, molto non mostrato ...)

class TooManyFields {
  private HashMap<String, Object> map = new HashMap<String, Object>();

  public setFoo(int i) { map.put("Foo", Integer.valueOf(i)); }
  public int getFoo()  { return map.get("Foo").intValue(); }

  public setBar(Sttring s) { map.put("Bar", s); }
  public String getBar()  { return map.get("Bar").toString(); }

  ... more getters and setters ...

  public boolean equals(Object o) {
    return (o instanceof TooManyFields) &&
           this.map.equals( ((TooManyFields)o).map);
}
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.