Guava poza całkiem przyjemnym zestawem idiomów funkcyjnych czy zastępników kodu oraz narzędzi wszelakich posiada też pewne przydatne pakiety 😉

Jednym z nich jest pakiet com.google.common.eventbus, który zawiera kompletną szynę zdarzeń. Jest to idealny zastępnik dla implementacji wzorca obserwatora z JDK.

I na początek jeszcze dwa słowa o tym dlaczego porównuję event busa do obserwatora. Ponieważ event bus jest specyficzną formą implementacji tego wzorca. Przy czym obserwowany obiekt nic nie wie o rzeczywistych obserwatorach. Jedyne co robi to wysyła informację do punktu dostępowego, że zaistniała jakaś godna uwago zmiana. Obserwatorzy muszą tylko wiedzieć z jakiego typu komunikatem będą mieli do czynienia.

Dlaczego Obserwator jest chujowy

Nie chodzi o sam wzorzec, ale o to jak został zaimplementowany w Javie. A został zaimplementowany bardzo źle. Problem leży w klasie Observable, która jest klasą. Zatem jeżeli chcesz użyć wzorca obserwatora to musisz ją rozszerzyć, a to zamyka w praktyce hierarchię dziedziczenia. W sumie inaczej tego nie dało się zrobić, bo model dziedziczenia w Javie jaki jest taki jest i lepszy nie będzie.
Co prawda można było to rozwiązać w trochę inny sposób np. za pomocą interfejsu, domyślnej implementacji i zalecenia implementacji przez delegację, lecz nikt na to nie wpadł. Szkoda, bo jest to kolejny mały kamyczek, który uwiera gdy przywdziejesz buty programisty Java.

Pierwsze kroki

Najprostszy program demonstrujący działanie Guava Event Bus będzie wyglądał mniej więcej tak:

Listing 1. Prosty program

public class App {

	public static void main(String[] args) {
		EventBus eb = new EventBus();
		eb.register(new StringEventHandler());
		eb.register(new IntegerEventHandler());
		eb.post(new StringEvent("Hello World!"));
		eb.post(new IntegerEvent(42));

	}
}

Co tu się dzieje? Sercem systemu jest klasa EventBus. Ona reprezentuje szynę. Można jej nadać identyfikator. Można utworzyć wiele instancji tej klasy i mieć wiele szyn. Można w końcu użyć AsyncEventBus, gdzie w konstruktorze przekazujemy Executor i otrzymujemy system współbieżny.
Kolejne dwie linie to rejestracja obiektów nasłuchujących. Jak to wygląda w bebechu to powiem za chwilę. Generalnie obiektem nasłuchującym może być każdy obiekt. Ostatnie dwie linie to wysyłanie zdarzeń. Tu podobnie jak w przypadku obiektów nasłuchujących można wysłać dowolny obiekt.

Widać tu pewną wadę tego rozwiązania. Otóż brak silnego powiązania pomiędzy kodem naszych obiektów nasłuchujących, a Guavą powoduje, że trzeba ręcznie rejestrować te obiekty w ramach szyny. Z drugiej strony zawsze pozostaje AOP na konstruktorze 😉

Obiekty nasłuchujące aka obserwatorzy

Obiekt nasłuchujący będzie powiadamiany o zdarzeniu przez szynę (tę w której go zarejestrowaliśmy). Co powoduje, że szyna rozróżnia kto ma dostać jakie powiadomienie i jak ma być ono obsłużone (którą metodę należy wywołać). Przyjrzyjmy się jednemu z naszych obserwatorów.

Listing 2. Obiekt nasłuchujący

class StringEventHandler {

	@Subscribe
	public void handleEvent(StringEvent stringEvent) {
		System.out.println(stringEvent.get());
	}

}

Cała „magia” rozwiązania leży sobie w adnotacji @Subscribe. W momencie rejestracji obiektu w szynie ta wyszukuje wszystkie metody oznaczone ta adnotacją. Metody muszą być publiczne i mieć tylko jeden argument. Jeżeli nie będzie spełniony pierwszy warunek, to metoda zostanie pominięta (pod spodem jest wywołanie getMethods, które zwróci metody publiczne). Jeżeli metoda będzie miała więcej argumentów albo nie będzie miała ich wcale to dostaniemy IllegalArgumentException. Na podstawie typu argumentu szyna będzie decydowała czy dane zdarzenie jest przeznaczone dla tego obserwatora.

Obiekty zdarzeń

Jak napisałem wcześniej zdarzenia mogą być dowolnego typu, a zatem nie będę dokładnie omawiał ich konstrukcji. Warto jednak omówić jakie cechy powinna mieć dobra klasa reprezentująca zdarzenie.
Po pierwsze, i oczywiste, powinna być niezmienna. Klasy niezmienne mają tą przyjemną własność, że możemy przyjąć iż nikt nam nic w niej nie namieszał.
Po drugie na zdarzenia nie nadają się obiekty reprezentujące typy prymitywne czy String. Choć obiekty te są niezmienne to jednak bezpośrednia wymiana zdarzeń za ich pomocą będzie trudna. Musimy wtedy dopisywać jakieś parsery, matchery diabli wiedzą co, żeby mieć pewność to co otrzymaliśmy rzeczywiście potrafimy obsłużyć.
Po trzecie w większych systemach warto „zainwestować” i wyposażyć nasze obiekty w UUID. Szczególnie jeżeli stosujemy „przepychanie” zdarzeń po między różnymi szynami.

Hierarchia i typy generyczne, a zdarzenia

Poza wspomnianymi już wcześniej dobrymi praktykami związanymi z tworzeniem zdarzeń należy jeszcze przyjrzeć się jak dziedziczenie wpływa na obsługę zdarzeń. Guava dość dobrze radzi sobie z klasycznym modelem dziedziczenia opartym o rozszerzanie klas. Dobrze widać to w poniższym programie (typy wiadomości dobrane tylko po to by nie komplikować)

Listing 3. Hierarchia klas zdarzeń

public class HierarchyApp {

	public static void main(String[] args) {
		EventBus eb = new EventBus();
		eb.register(new IntHandler());
		eb.register(new NumberHandler());
		eb.post(1);
		eb.post(2L);
	}
}

class IntHandler{

	@Subscribe
	public void forInt(Integer i){
		System.out.println("Integer " + i);
	}

}
class NumberHandler{

	@Subscribe
	public void forInt(Number i){
		System.out.println("Number " + i);
	}

}

W przypadku pierwszej wiadomości, która jest typu Integer, który rozszerza Number w efekcie oba handlery otrzymają tą wiadomość. Druga wiadomość jest typu Long zatem otrzyma go tylko handler obsługujący nad typ.

Drugim modelem dziedziczenia jest generyczność. Z tym Guava całkowicie sobie nie radzi. Jeżeli w naszym pierwszym przykładzie zamienilibyśmy obsługę StringEvent i IntegerEvent tak by zamiast tych klas przyjmowały generyczny Event<T&gy; to oba obiekty nasłuchujące otrzymały by oba komunikaty. Efekt… kboom!

DeadEvent

Ostatnim podstawowym elementem do omówienia jaki nam został jest sposób obsługi wiadomości nieobsłużonych. Najprostszym przypadkiem takiej wiadomości jest przypadek gdy wysłaliśmy wiadomość, ale nie ma obserwatora, który może ja obsłużyć. W takim przypadku wiadomość zostaje opakowana w klasę DeadEvent i wysłana do klasy, która potrafi ją obsłużyć (sami musimy ją napisać i zarejestrować w szynie).

Podsumowanie

Guava Event Bus to świetne rozwiązanie pozwalające na wyrugowanie z naszego kodu silnej zależności od obserwatora z podstawowego API. Zaletą jest też stosunkowa prostota wdrożenia tego rozwiązania oraz całkiem rozsądna wydajność.
Wadą brak obsługi typów generycznych.

Jak spodobał Ci się ten wpis to użyj guzików poniżej by podzielić się nim ze swoimi znajomymi.