Wstęp do Google Guice

post_img

Guice jest biblioteką od Google, której ideę stanowi możliwość zastąpienia różnego rodzaju fabryk obiektów, tworzenia obiektów za pomocą operatora new. Zapewnia ona ponadto mechanizmy zbliżone do dependency injection.

Wiadomo, że najlepiej człowiek uczy się na przykładzie, więc omówię tę bibliotekę podpierając się odniesieniem do prostego projektu. Link do całej aplikacji dostępny jest na końcu artykułu.

Załóżmy, że mamy w naszej aplikacji jakieś zdarzenia które chcemy obsługiwać i wysyłać powiadomienia. Mamy więc abstrakcyjny byt Event, przydałby się manager zarządzania powiadomieniami i oczywiście mechanizmy obsługi zdarzeń – EventHandler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package pl.atena.guice.demo.api;

public interface Event {
    String getCaller();
}
//...
public interface NotificationManager {
    void sendMail(String userName);
}
//...
public class EventHandler {
   
    private NotificationManager notificationManager;
    private Event event;
   
    public EventHandler(NotificationManager notificationManager, Event event) {
        this.notificationManager = notificationManager;
        this.event = event;
    }
   
    public void handle(){
        //do some event handling
        //send notification
        notificationManager.sendMail(event.getCaller());
    }
}
//...

Do czego w takim razie jest nam tu potrzebne Guice? Przecież w czasie działania aplikacji jeżeli nasz kontener wspiera w jakiś wstrzykiwanie instancji, mógłby więc też wrzucić nam odpowiednie elementy i niczym nie musielibyśmy się martwić. No tak, ale co w sytuacji, kiedy nasz kontener tak po prostu tego nie potrafi? A jeżeli w ogóle nie mamy kontenera? Co z testami jednostkowymi, które chcemy uruchamiać bez niego? Do tego właśnie nadaje się Guice. Zamiast w aplikacji wykonywać rzeczy takie jak

1
2
3
4
5
6
7
8
public class MainApp {
    public static void main(String[] args) {
        Event event = new CreationEvent("Creation Event caller");
        NotificationManager manager = new DefaultNotificationManager();
        EventHandler handler = new EventHandler(manager, event);
        handler.handle();
    }
}

a potem jeszcze „kombinować” przy testach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//test obslugi eventow, nie chcemy tutaj testowac mailingu
public class TestEventHandling {

    public static void main(String[] args) {
        Event event = new CreationEvent("Creation Event caller");
                NotificationManager manager = new FakeNotificationManager();
                EventHandler handler = new EventHandler(manager, event);
                handler.handle();
    }
   
}
//...
//test mailingu, jakie to są eventy nas nie obchodzi
public class TestNotificationManager {

    public static void main(String[] args) {
        Event event = new FakeEvent();
                NotificationManager manager = new DefaultNotificationManager();
                EventHandler handler = new EventHandler(manager, event);
                handler.handle();
    }
   
}

możemy posłużyć się Guice, by to on zadbał o wszystkie konieczne elementy dla naszej aplikacji i testów. Czy nie lepiej zrobić coś takiego:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MainApp {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new DefaultEventHandlerModule());
        EventHandler instance = injector.getInstance(EventHandler.class);
        instance.handle();
    }
}
//....
public class TestEventHandling {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new TestEventHandlerModule());
        EventHandler instance = injector.getInstance(EventHandler.class);
        instance.handle();
    }
   
}
//....
public class TestNotificationManager {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new TestNotificationManagerModule());
        EventHandler instance = injector.getInstance(EventHandler.class);
        instance.handle();
    }
   
}

Takie podejście upraszcza nam logikę testów i odcina je od faktycznej implementacji. Czy ktoś jest w stanie sobie wyobrazić sytuację w której handler będzie miał dziesięć różnych elementów? Wszystkie chcemy testować, a po jakimś czasie okazuje się, że dwa z nich sa niepotrzebne, a trzy nowe musimy dodać. Doprowadziłoby to do zmian wielu testów, nie mówiąc o zmianach w logice biznesowej. Utrzymywanie tego bytu stałoby się mozolne. A w większych aplikacjach takich bytów bywa przecież więcej. Guice pozwala uniknąć tych problemów, musimy zadbać tylko o parę rzeczy. Po pierwsze – obiekty powinny posiadać konstruktory bezargumentowe lub adnotowane przez @Inject.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CreationEvent implements Event{

    private String caller;
    @Inject
    public CreationEvent(String caller) {
        this.caller = caller;
    }
   
    @Override
    public String getCaller() {
        return caller;
    }  
}
//...
public class DefaultNotificationManager implements NotificationManager{

    @Override
    public void sendMail(String userName) {
        // fancy mail sending logic
        System.out.println("sending mail from class " + this.getClass().getName() + " to " + userName);
    }

}

Po drugie – musimy dla każdej interesującej nas kombinacji parametrów obiektu stworzyć moduł, który będzie wiedział jak utworzyć naszą instancję. Czyli dla kawałka kodu

1
2
Injector injector = Guice.createInjector(new TestNotificationManagerModule());
EventHandler instance = injector.getInstance(EventHandler.class);

potrzebujemy TestNotificationManagerModule. Każdy moduł powinien rozszerzać klasę AbstractModule oraz implementować metodę configure(), w której określamy jakie klasy chcemy posiadać w czasie runtime’u ukryte pod interfejsami

1
2
3
4
5
6
7
8
9
public class TestNotificationManagerModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(NotificationManager.class).to(DefaultNotificationManager.class);
        bind(Event.class).to(FakeEvent.class);
    }

}

W przypadku klas które posiadają konstruktory z parametrami możemy wykorzystać nieco inne podejście – metody @Provides

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DefaultEventHandlerModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(NotificationManager.class).to(DefaultNotificationManager.class);
    }
   
    @Provides
    Event provideEvent(){
        CreationEvent event = new CreationEvent("Creation Event Caller");
        return event;
    }

}

Metody takie zastępują wywołanie bind() – gdy moduł będzie potrzebował implementacji Event odszuka odpowiednią metodę z adnotacją @Provides i wywoła ją w celu pozyskania elementu.

W ten sposób możemy utworzyć dowolne kombinacje elementów podstawiając wymagane implementacje, zależnie od naszych potrzeb w danej chwili. Co więcej zmiana mechanizmów tworzenia wstrzykiwanego komponentu sprowadza się do zmiany modułów, które są zazwyczaj prostymi klasami, i nie musimy dokonywać dodatkowych modyfikacji w kodzie z logiką biznesową.

Cały projekt można pobrać z załączników żeby zobaczyć jak moduły załatwiają sprawę przydzielania odpowiednich implementacji do interfejsów zależnie od potrzeb.

Guice posiada kilka mechanizmów wstrzykiwań

  • linked bindings – tak jak w podanym przykładzie
  • instance bindings – możliwość przywiązania klasy do jej konkretnej instancji. Przydatne w łączeniu z adnotowaniem parametrów (o tym niżej)
  • 1
    bind(String.class).annotatedWith(Names.named("JDBC URL")).toInstance("jdbc:mysql://localhost/pizza");
  • @Provides methods – jak w przykładzie, wykorzystanie metod oznaczonych @Provides zamiast bind(). Przydatne przy większych obiektach które posiadają proste elementy w konstruktorach, oraz w sytuacji, kiedy chcemy je przed użyciem skonfigurować
  • 1
    2
    3
    4
    5
    @Provides
    Event provideEvent(){
        CreationEvent event = new CreationEvent("Creation Event Caller");
        return event;
    }
  • klasy Provider – mechanizm podobny do metod @Provides, jednak dla zdecydowanie większych i bardziej złożonych konstrukcji. Klasa taka powinna rozszerzać interfejs Provider<T> i posiadać metodę get() która zwraca wymagany element T. Klasy takie mogą jak najbardziej być adnotowane przez @Inject by uprościć sobie ich tworzenie, np
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
      private final Connection connection;

      @Inject
      public DatabaseTransactionLogProvider(Connection connection) {
        this.connection = connection;
      }

      public TransactionLog get() {
        DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
        transactionLog.setConnection(connection);
        return transactionLog;
      }
    }

    i w module przywiązujemy interfejs nie do konkretnej klasy, ale do klasy Providera

    1
    2
    3
    4
    @Override
      protected void configure() {
        bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
      }
  • untargeted bindings – wiązania, które nie określają implementacji.
  • 1
    bind(CreditCardProcessor.class)

    Przydatne, jeżeli posiadamy interfejsy adnotowane @ImplementedBy i @ProvidedBy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @ImplementedBy(PayPalCreditCardProcessor.class)
    public interface CreditCardProcessor {
      ChargeResult charge(String amount, CreditCard creditCard)
          throws UnreachableException;
    }
    //...
    @ProvidedBy(DatabaseTransactionLogProvider.class)
    public interface TransactionLog {
      void logConnectException(UnreachableException e);
      void logChargeResult(ChargeResult result);
    }

Adnotowanie parametrów służy rozróżnieniu wiązań – możemy przecież chcieć posiadać w ramach modułu różne implementacje zależnie od sytuacji.
Więcej o adnotowaniu parametrów w dokumentacji

Zasoby i bibliografia:

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *