È una cattiva abitudine a (sopra) usare la riflessione?

14

È una buona pratica usare la riflessione se si riduce notevolmente la quantità di codice boilerplate?

Fondamentalmente vi è un compromesso tra prestazioni e forse leggibilità da un lato e astrazione / automazione / riduzione del codice dello standard sull'altro lato.

Modifica: Ecco un esempio di uso consigliato della riflessione .

Per fare un esempio, supponiamo che esista una classe astratta Base che ha 10 campi e ha 3 sottoclassi SubclassA , SubclassB e SubclassC ciascuno con 10 campi diversi; sono tutti fagioli semplici. Il problema è che ottieni due riferimenti di tipo Base e vuoi vedere se i loro oggetti corrispondenti sono dello stesso (sotto) tipo e sono uguali.

Come soluzioni c'è la soluzione grezza in cui si controlla prima se i tipi sono uguali e poi si controllano tutti i campi oppure si può usare il reflection e vedere dinamicamente se sono dello stesso tipo e iterare su tutti i metodi che iniziano con "get" "(convenzione sulla configurazione), chiamali su entrambi gli oggetti e chiama equals sui 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");
        }
}
    
posta m3th0dman 01.04.2013 - 11:34
fonte

5 risposte

23

Reflection è stato creato per uno scopo specifico, per scoprire la funzionalità di una classe sconosciuta in fase di compilazione, simile a quello che le funzioni dlopen e dlsym fanno in C. Qualsiasi utilizzo al di fuori di quello dovrebbe essere pesantemente esaminato.

Ti è mai venuto in mente che gli stessi progettisti Java abbiano riscontrato questo problema? Ecco perché praticamente ogni classe ha un metodo equals . 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 vuole l'uguaglianza personalizzata dovrebbe implementare un metodo equals . Alla fine, vorrai mettere gli oggetti in un set, o usarli come indice di hash, quindi dovrai comunque implementare equals . Altre lingue lo fanno in modo diverso, ma Java usa equals . Dovresti attenersi alle convenzioni della tua lingua.

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

    
risposta data 01.04.2013 - 15:01
fonte
10

L'uso eccessivo della riflessione dipende probabilmente dalla lingua utilizzata. Qui stai usando Java. In questo caso, la riflessione dovrebbe essere usata con attenzione perché spesso è solo una soluzione alternativa per un cattivo design.

Quindi, stai confrontando diverse classi, questo è un problema perfetto per il metodo che sovrascrive . Si noti che le istanze di due classi diverse non dovrebbero mai essere considerate uguali. Puoi confrontare per l'uguaglianza solo se hai istanze della stessa classe. Vedi link per un esempio su come implementare il confronto di uguaglianza correttamente.

    
risposta data 01.04.2013 - 11:47
fonte
0

L'abuso di qualcosa per definizione è sbagliato, giusto? Quindi sbarazzarsi di (oltre) per ora.

Chiameresti l'uso interno di riflessione incredibilmente pesante della primavera come una cattiva abitudine?

Spring 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, probabilmente l'uso negligente del riflesso tornerà a morderti nel sedere).

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

    
risposta data 10.12.2018 - 18:00
fonte
0

Penso che la maggior parte di queste risposte non trovi il punto.

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

  2. Tuttavia, per una classe con molti campi, questa può essere noiosa.

  3. Quindi dipende .

    • Se hai solo bisogno di equals e hashcode raramente , è pragmatico, e probabilmente o.k., utilizzare un calcolo di riflessione generico. Almeno un primo passaggio veloce e sporco.
    • ma se hai bisogno di uguali molto, e.g questi oggetti vengono inseriti in HashTables, in modo che le prestazioni siano un problema, dovresti assolutamente scrivere il codice. sarà molto più veloce.
  4. Un'altra possibilità: se le classi hanno davvero così tanti campi che scrivere un uguale è noioso, prendi in considerazione

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

es. ( nota : ignorare i controlli Null, probabilmente dovresti usare Enum anziché le 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);
}
    
risposta data 10.12.2018 - 23:09
fonte
0

Penso che tu abbia due problemi qui.

  1. Quanto dovrei avere il codice dinamico vs statico?
  2. Come posso esprimere una versione personalizzata di uguaglianza?

Dinamico vs codice statico

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

Da un lato il tuo compilatore è molto bravo a catturare tutti i tipi di codice errato. Lo fa attraverso varie forme di analisi, l'analisi del tipo è comune. Sa che non puoi utilizzare un oggetto Banana nel codice che si aspetta un Cog . Ti dice questo attraverso un errore di compilazione.

Ora può farlo solo quando può dedurre sia il tipo accettato, sia il tipo dato dal contesto. Quanto si può dedurre, e quanto generale è l'inferenza, dipende in gran parte dalla lingua utilizzata. Java può dedurre informazioni di tipo tramite meccanismi come l'ereditarietà, le interfacce e i generici. Il chilometraggio varia, alcune lingue forniscono meno meccanismi e altre forniscono di più. Si riduce a ciò che il compilatore può sapere essere vero.

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

Se rendere il tuo codice molto specifico o molto generale si riduce al problema che stai cercando di gestire. Se puoi facilmente esprimerlo usando il sistema dei tipi, fallo. Lascia che il compilatore giochi i suoi punti di forza e ti aiuti. Se il sistema di tipi non è in grado di sapere in anticipo (codice estraneo), o il sistema di tipi è inadatto per un'implementazione generale del tuo algoritmo, la riflessione (e altri mezzi dinamici) sono lo strumento giusto da usare.

Tieni presente 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 da spagnolo, francese e cantonese per esprimere con precisione i tuoi pensieri. Il contesto dirà molto al tuo amico, ma potrebbe anche non sapere come gestire quelle parole che portano a tutti i tipi di equivoci. Si tratta di quei fraintendimenti migliori che non si spiegano queste idee in inglese usando più parole?

Uguaglianza personalizzata

Pur comprendendo che Java fa molto affidamento sul metodo equals per confrontare in generale due oggetti non sempre adatti in un dato contesto.

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

Per quanto riguarda l'implementazione del tuo comparatore dipenderà da cosa stai confrontando e da come.

  • Può essere applicato a qualsiasi due oggetti indipendentemente dalla specifica implementazione di equals .
  • Potrebbe implementare un metodo di confronto generale (basato su riflessioni) per la gestione di due oggetti.
  • Le funzioni di confronto delle piastre possono essere aggiunte per tipi di oggetto comunemente confrontati, garantendo la sicurezza e l'ottimizzazione del tipo.
risposta data 11.12.2018 - 05:08
fonte

Leggi altre domande sui tag