Assisted Injection po bożemu
Tak jak obiecałem tym razem zajmiemy się mechanizmem Assisted Injection w wersji „po bożemu”.
Motywacja
Czasami stajemy przed konieczności wyboru metody tworzenia obiektów pomiędzy w pełni automatycznym procesem kontrolowanym przez mechanizm kontenera wstrzykiwania zależności (DI), a ręcznym tworzeniem obiektów. W tym drugim przypadku jeżeli korzystamy z mechanizmu DI istnieje niezerowa szansa na to, że kontener dostarcza jakiegoś w miarę standardowego mechanizmu fabrykującego. W przypadku Google Guice jak i w przypadku JSR-330 jest to interfejs Provider. Implementując ten interfejs możemy stworzyć własną fabrykę, w której dowolnie możemy konfigurować obiekty. Wady tego rozwiązania ujawniają się w momencie gdy nasze obiekty powinny być zależne od obiektów zarządzanych przez DI oraz dodatkowo część własności ustawiamy na podstawie informacji, które możemy pozyskać dopiero po uruchomieniu. W takim przypadku implementacja providera musi mieć dostęp do źródła obiektów zarządzanych przez DI (kolejne providery?), ale też do informacji pozyskanych z poza aplikacji. To drugie oznacza, konieczność tworzenia instancji proviaderów w zasięgu NOSCOPE, ale co ważniejsze każdorazowe dokonfigurowywanie providera przed wywołaniem metody get. Względnie możemy stworzyć fabrykę. W praktyce nie zmienia to nic jeśli chodzi o wiązanie obiektów. Nadal musimy robić to samodzielnie.
Jak widać takie rozwiązanie jest kłopotliwe w implementacji, a co więcej może prowadzić do różnych ciekawych „efektów specjalnych”, które będą manifestacjami tzw. „Haisenberg bug”. Przydała by nam się zatem funkcjonalność, która łączyła by w sobie automatyczne wstrzykiwanie z możliwością ręcznego ustawienia części pól. Taką funkcjonalność dostarcza Google Guice pod nazwą Assisted Injection (AI).
Chwila teorii (przez machanie rencyma)
W klasycznym układzie z providerem by uzyskać obiekt wstrzykujemy sobie instancję providera i następnie wywołujemy nieposiadającą parametrów metodę get. W mniej ortodoksyjnym środowisku możemy zamiast providera wstrzyknąć fabrykę, która będzie miała kilka różnych metod pozwalających tworzyć obiekty w zależności od potrzeb. Jak już wspomniałem problemem będzie wiązanie obiektów zarządzanych przez kontener oraz dodatkowo zmuszenie kontenera do zarządzania utworzonym przez nas obiektem.
Wykorzystując AI pozbywamy się tych problemów. Nasze zadanie ogranicza się do zdefiniowania interfejsu fabryki oraz skonfigurowania modułu. Guice sam wygeneruje sobie odpowiedni kod. Mechanizm można zatem opisać w następujący sposób:
- Tworzymy interfejs fabryki.
- Konfigurujemy moduł Guice.
- Tam gdzie potrzeba wstrzykujemy nasz interfejs.
- Wołamy metodę do której przekazujemy w parametrach niezarządzaną część.
Brzmi skomplikowanie? W Guice 3 jest to banalnie proste.
Praktyka
Załóżmy, że mamy sobie klasę AIO, która reprezentuje coś i udostępnia operacje CRUD (taki kulawy active record). Potrzebujemy zatem z jednej strony dostępu do bazy danych, a z drugiej obiekty możemy tworzyć w oparciu o informacje od użytkownika (np. formularz wyszukiwarki).
Listing 1. klasa AIO
@Entity
public class AIO {
@Inject
private transient EntityManager em;
@Id
private String name;
private String pass;
// konstruktory - patrz opis poniżej
public void delete() {
//...
}
public AIO pass(String newPass) {
//...
}
public void save() {
//...
}
public static AIO get(String name) {
//...
}
}
Mamy tu jakąś metodę get pozwalającą na wyszukanie i załadowanie obiektu mamy save i delete mamy, w końcu pass spełniającą rolę settera. nic nadzwyczajnego. Problem polega na tym, że EntityManager powinien trafić z kontenera DI, a znowuż pole name uzyskamy zapewne od użytkownika. W dodatku na podstawie name będziemy wstanie wyszukać resztę pól…
Definicja fabryki
Zdefiniujmy sobie na początek klika konstruktorów, z pomocą których będziemy tworzyć obiekty.
Listing 2. konstruktory AIO
private AIO() {
}
@AssistedInject
private AIO(@Assisted("name") String name) {
this.name = name;
}
@AssistedInject
private AIO(@Assisted("name") String name, @Assisted("pass") String pass) {
this.name = name;
this.pass = pass;
}
Pierwszy wynika z wymagań JPA. Kolejne dwa pozwalają na tworzenie obiektu w zależności o tego czy chcemy ustawić pole pass, czy nie…
WTF! One są prywatne!
Oczywiście. W końcu chcemy by nasze obiekty były tworzone przez DI, a nie ręcznie. Prywatny konstruktor to jedyne rozwiązanie. Spoko… teraz czas na fabrykę.
Fabryka
Tu leży pies pogrzebany i całe piękno AI. Wystarczy, że stworzymy tylko interfejs fabryki, by nie musieć martwić się o to jak tworzone są obiekty.
Listing 3. Interfejs AIOFactory
interface AIOFactory {
AIO make(@Assisted("name") String name);
AIO make(@Assisted("name") String name, @Assisted("pass") String pass);
}
Poszczególne metody odpowiadają konstruktorom. Dodatkowe nazwy w adnotacjach Assisted są wymagane jeżeli parametry mają taki sam typ.
konfiguracja
Ostatnim krokiem jest konfiguracja modułu. AI jest rozszerzeniem Guice i niema go w standardowej paczce. To tak w ramach przypomnienia. W naszym module wystarczy dodać jedną linię.
Listing 4. konfiguracja modułu
install(new FactoryModuleBuilder().implement(AIO.class, AIO.class).build(AIOFactory.class));
Później wstrzykujemy AIOFactory i korzystamy jak ze zwykłej fabryki…
Podsumowanie
Mechanizm Assisted Injection jest bardzo wygodnym rozwiązaniem (szczególnie w Guice 3) jeżeli musimy tworzyć obiekty zależne zarówno od innych, zarządzanych przez DI, jak i danych otrzymywanych z poza aplikacji. Warto przemyśleć użycie tego rozszerzenia, bo ułatwia życie.