Fondamentalmente, riflessione significa usare il codice del tuo programma come dati.
Pertanto, l'uso della riflessione potrebbe essere una buona idea quando il codice del programma è un'utile fonte di dati. (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. Potresti 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 codice molto generico, e ogni volta che cambi la classe, devi aggiornare il codice. In realtà, potresti descrivere ciò che questo codice fa come
- crea un elemento XML con il nome della classe
- per ogni proprietà della classe
- crea un elemento XML con il nome della proprietà
- inserisce 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 ti permette di ispezionare i tipi usando i metodi della classe Class
.
Altri casi d'uso:
- definisce 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 test unitario
Tuttavia, la piena riflessione 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 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 infatti hai anche una seconda implementazione:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Ora vuoi anche qualche output di registro; vuoi semplicemente un messaggio di log ogni volta che viene chiamato un metodo. È possibile aggiungere in modo esplicito l'output del registro ad ogni metodo, ma ciò sarebbe fastidioso e dovresti farlo due volte; una volta per ogni implementazione. (Quindi ancora di più quando aggiungi ulteriori 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
}
Anche in questo caso, c'è un modello ripetitivo che può essere descritto da un algoritmo:
- un proxy logger è una classe che implementa un'interfaccia
- ha un costruttore che prende un'altra implementazione dell'interfaccia e un logger
- per ogni metodo nell'interfaccia
- l'implementazione registra un messaggio "$ methodname called"
- e quindi chiama lo stesso metodo sull'interfaccia interna, passando lungo tutti gli argomenti
e l'input di questo algoritmo è la definizione dell'interfaccia.
Reflection ti permette di definire una nuova classe usando questo algoritmo. Java ti permette di farlo usando i metodi della classe java.lang.reflect.Proxy
, e ci sono librerie che ti danno ancora più energia.
Quindi quali sono i lati negativi del riflesso?
- Il tuo codice diventa più difficile da capire. Sei un livello di astrazione ulteriormente rimosso dagli effetti concreti del tuo codice.
- Il codice diventa più difficile da eseguire il debug. Specialmente con le librerie generatrici di codice, il codice che è stato 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 posizionare i breakpoint).
- Il tuo codice diventa più lento. La lettura dinamica delle informazioni sul tipo e l'accesso ai campi tramite i relativi handle di runtime invece dell'accesso hard-coding è più lento. La generazione di codice dinamico può mitigare questo effetto, al costo di essere persino più difficile eseguire il debug.
- Il tuo codice potrebbe diventare più fragile. L'accesso alla riflessione dinamica non è controllato dal compilatore, ma genera errori in fase di esecuzione.