Zasadniczo refleksja oznacza użycie kodu programu jako danych.
Dlatego użycie refleksji może być dobrym pomysłem, gdy kod programu jest użytecznym źródłem danych. (Ale są kompromisy, więc nie zawsze może to być dobry pomysł).
Rozważmy na przykład prostą klasę:
public class Foo {
public int value;
public string anotherValue;
}
i chcesz z niego wygenerować XML. Możesz napisać kod do wygenerowania 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;
}
Ale to dużo kodu z podstawowymi danymi i za każdym razem, gdy zmieniasz klasę, musisz aktualizować kod. Naprawdę możesz opisać, co robi ten kod jako
- utwórz element XML o nazwie klasy
- dla każdej właściwości klasy
- utwórz element XML o nazwie właściwości
- wstaw wartość właściwości do elementu XML
- dodaj element XML do katalogu głównego
To jest algorytm, a dane wejściowe algorytmu to klasa: potrzebujemy jego nazwy oraz nazw, typów i wartości jego właściwości. Tu pojawia się refleksja: daje dostęp do tych informacji. Java pozwala sprawdzać typy przy użyciu metod Class
klasy.
Kilka innych przypadków użycia:
- zdefiniuj adresy URL na serwerze sieciowym na podstawie nazw metod klasy, a parametry adresów URL na podstawie argumentów metody
- przekonwertować strukturę klasy na definicję typu GraphQL
- wywołuje każdą metodę klasy, której nazwa zaczyna się od „test” jako test jednostkowy
Jednak pełne odzwierciedlenie oznacza nie tylko spojrzenie na istniejący kod (który sam w sobie jest znany jako „introspekcja”), ale także modyfikowanie lub generowanie kodu. W tym celu istnieją dwa znaczące przypadki użycia w Javie: serwery proxy i makiety.
Powiedzmy, że masz interfejs:
public interface Froobnicator {
void froobnicateFruits(List<Fruit> fruits);
void froobnicateFuel(Fuel fuel);
// lots of other things to froobnicate
}
i masz implementację, która robi coś interesującego:
public class PowerFroobnicator implements Froobnicator {
// awesome implementations
}
W rzeczywistości masz też drugą implementację:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Teraz potrzebujesz także danych wyjściowych dziennika; po prostu chcesz komunikat dziennika za każdym razem, gdy wywoływana jest metoda. Możesz jawnie dodać dane wyjściowe dziennika do każdej metody, ale byłoby to irytujące i musiałbyś to zrobić dwa razy; raz na każde wdrożenie. (A więc jeszcze więcej, jeśli dodasz więcej implementacji).
Zamiast tego możesz napisać 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
}
Ponownie jednak istnieje powtarzalny wzorzec, który można opisać algorytmem:
- proxy rejestratora to klasa implementująca interfejs
- ma konstruktor, który przyjmuje inną implementację interfejsu i rejestrator
- dla każdej metody w interfejsie
- implementacja rejestruje komunikat „$ nazwa metody o nazwie”
- a następnie wywołuje tę samą metodę na interfejsie wewnętrznym, przekazując wszystkie argumenty
a wejściem tego algorytmu jest definicja interfejsu.
Refleksja pozwala zdefiniować nową klasę za pomocą tego algorytmu. Java pozwala to zrobić przy użyciu metod java.lang.reflect.Proxy
klasy, a istnieją biblioteki, które dają jeszcze więcej mocy.
Jakie są wady refleksji?
- Twój kod staje się trudniejszy do zrozumienia. Masz jeden poziom abstrakcji, który jest dalej usuwany z konkretnych efektów twojego kodu.
- Kod staje się trudniejszy do debugowania. Zwłaszcza w bibliotekach generujących kod wykonywany kod może nie być kodem, który napisałeś, ale kodem, który wygenerowałeś, a debugger może nie być w stanie pokazać Ci tego kodu (lub pozwolić ci umieszczać punkty przerwania).
- Twój kod staje się wolniejszy. Dynamiczne czytanie informacji o typie i uzyskiwanie dostępu do pól za pomocą ich uchwytów środowiska wykonawczego zamiast dostępu do kodowania jest wolniejsze. Dynamiczne generowanie kodu może złagodzić ten efekt, kosztem jeszcze trudniejszego debugowania.
- Twój kod może stać się bardziej kruchy. Dynamiczny dostęp do odbicia nie jest sprawdzany przez kompilator, ale generuje błędy w czasie wykonywania.