Fondamentalmente, riflessione significa usare il codice del programma come dati.
Pertanto, l'utilizzo di reflection potrebbe essere una buona idea quando il codice del programma è una fonte di dati utile. (Ma ci sono dei compromessi, quindi potrebbe non essere sempre una buona idea.)
Ad esempio, considera una classe semplice:
public class Foo {
public int value;
public string anotherValue;
}
e vuoi generare XML da esso. È possibile scrivere codice per generare l'XML:
public XmlNode generateXml(Foo foo) {
XmlElement root = new XmlElement("Foo");
XmlElement valueElement = new XmlElement("value");
valueElement.add(new XmlText(Integer.toString(foo.value)));
root.add(valueElement);
XmlElement anotherValueElement = new XmlElement("anotherValue");
anotherValueElement.add(new XmlText(foo.anotherValue));
root.add(anotherValueElement);
return root;
}
Ma questo è un sacco di codice boilerplate e ogni volta che cambi classe, devi aggiornare il codice. Davvero, potresti descrivere cosa fa questo codice
- creare un elemento XML con il nome della classe
- per ogni proprietà della classe
- creare un elemento XML con il nome della proprietà
- inserire il valore della proprietà nell'elemento XML
- aggiungi l'elemento XML alla radice
Questo è un algoritmo e l'input dell'algoritmo è la classe: abbiamo bisogno del suo nome e dei nomi, tipi e valori delle sue proprietà. È qui che entra in gioco la riflessione: ti dà accesso a queste informazioni. Java consente di ispezionare i tipi utilizzando i metodi della Class
classe.
Alcuni altri casi d'uso:
- definire gli URL in un server Web in base ai nomi dei metodi di una classe e i parametri URL in base agli argomenti del metodo
- converte la struttura di una classe in una definizione di tipo GraphQL
- chiama ogni metodo di una classe il cui nome inizia con "test" come caso di unit test
Tuttavia, la riflessione completa significa non solo guardare il codice esistente (che di per sé è noto come "introspezione"), ma anche modificare o generare codice. Ci sono due casi d'uso importanti in Java per questo: proxy e mock.
Supponiamo che tu abbia un'interfaccia:
public interface Froobnicator {
void froobnicateFruits(List<Fruit> fruits);
void froobnicateFuel(Fuel fuel);
// lots of other things to froobnicate
}
e hai un'implementazione che fa qualcosa di interessante:
public class PowerFroobnicator implements Froobnicator {
// awesome implementations
}
E in effetti hai anche una seconda implementazione:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Ora vuoi anche un po 'di output del log; vuoi semplicemente un messaggio di registro ogni volta che viene chiamato un metodo. È possibile aggiungere in modo esplicito l'output del registro a ogni metodo, ma sarebbe fastidioso e dovresti farlo due volte; una volta per ogni implementazione. (Quindi ancora di più quando aggiungi più implementazioni.)
Invece, puoi scrivere un proxy:
public class LoggingFroobnicator implements Froobnicator {
private Logger logger;
private Froobnicator inner;
// constructor that sets those two
public void froobnicateFruits(List<Fruit> fruits) {
logger.logDebug("froobnicateFruits called");
inner.froobnicateFruits(fruits);
}
public void froobnicateFuel(Fuel fuel) {
logger.logDebug("froobnicateFuel( called");
inner.froobnicateFuel(fuel);
}
// lots of other things to froobnicate
}
Ancora una volta, tuttavia, esiste un modello ripetitivo che può essere descritto da un algoritmo:
- un proxy logger è una classe che implementa un'interfaccia
- ha un costruttore che richiede un'altra implementazione dell'interfaccia e un logger
- per ogni metodo nell'interfaccia
- l'implementazione registra un messaggio "$ methodname chiamato"
- e quindi chiama lo stesso metodo sull'interfaccia interna, passando tutti gli argomenti
e l'input di questo algoritmo è la definizione dell'interfaccia.
Reflection ti consente di definire una nuova classe usando questo algoritmo. Java ti consente di farlo usando i metodi della java.lang.reflect.Proxy
classe e ci sono librerie che ti danno ancora più potenza.
Quindi quali sono gli svantaggi della riflessione?
- Il tuo codice diventa più difficile da capire. Sei un livello di astrazione ulteriormente rimosso dagli effetti concreti del tuo codice.
- Il tuo codice diventa più difficile da eseguire il debug. Soprattutto con le librerie che generano codice, il codice che viene eseguito potrebbe non essere il codice che hai scritto, ma il codice che hai generato e il debugger potrebbe non essere in grado di mostrarti quel codice (o di lasciare dei punti di interruzione).
- Il tuo codice diventa più lento. La lettura dinamica delle informazioni sui tipi e l'accesso ai campi tramite i loro handle di runtime invece dell'accesso con codifica rigida è più lento. La generazione di codice dinamico può mitigare questo effetto, a costo di essere ancora più difficile da eseguire il debug.
- Il tuo codice potrebbe diventare più fragile. L'accesso al riflesso dinamico non viene verificato dal compilatore, ma genera errori in fase di esecuzione.