Wzorzec obserwatora w Seam
Omawiając wstrzykiwanie zależności wspomniałem coś o obserwatorach. Dzisiaj postaram się omówić to zagadnienie dokładniej.
Obserwator w Seam Framework wygląda trochę inaczej niż klasyczny wzorzec projektowy o tej samej nazwie. Ogólna idea jest podobna, ale zastosowane mechanizmy różnią się znacznie.
Problem
W aplikacji internetowej należy dodać powiadomienia o ostatnich zmianach, jakie zaszły w bazie danych. Informacje mogą być bardzo złożone (włączając w to przyciski z akcjami), więc potrzebny będzie komponent z listą powiadomień oraz szereg komponentów z informacjami, implementujących interfejs Info:
public interface Info {
public String getText();
}
Szkielet komponentu listy powiadomień (notifications) prezentuje się następująco:
@Name("notifications")
@Scope(ScopeType.PAGE)
public class Notifications {
private List<Info> informationList;
@Unwrap
public List<Info> getList() {
if (informationList == null)
informationList =
new ArrayList<Info>();
return informationList;
}
}
Należy też utworzyć odpowiedni widok, wyświetlający kolejne elementy listy:
<ui:composition
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:rich="http://richfaces.org/rich"
template="layout/template.xhtml">
<ui:define name="body">
<rich:dataList
value="#{notifications}"
var="_info">
#{_info.text}
</rich:dataList>
</ui:define>
</ui:composition>
Problemem jest dodawanie po jednej instancji informacji (powiadomienia) każdego typu – interfejs Info będzie posiadał różne implementacje.
Rozwiązanie 1
Najprościej byłoby wstrzyknąć komponenty różnych typów do klasy Notifications:
@In(create = true) private Info infoImplementation1; @In(create = true) private Info infoImplementation2; @In(create = true) private Info infoImplementation3; // ...
Następnie można każdy z komponentów dodać do nowo tworzonej listy:
@Unwrap
public List<Info> getList() {
if (informationList == null) {
informationList =
new ArrayList<Info>();
informationList.add(infoImplementation1);
informationList.add(infoImplementation2);
informationList.add(infoImplementation3);
// ...
}
return informationList;
}
To rozwiązanie jest po prostu straszne.
Zgodnie z zasadą otwarcia i zamknięcia, Rozszerzenie funkcjonalności programu nie może powodować zmian w istniejącym kodzie. Tutaj każda nowa implementacja powiadomienia, to dodanie nowego pola do klasy Notifications i jeszcze dodatkowa linijka kodu w metodzie getList().
Rozwiązanie 2
Drugim możliwym wyjściem z sytuacji jest zastosowanie metody Component.getInstance():
@Unwrap
public List<Info> getList() {
if (informationList == null) {
informationList =
new ArrayList<Info>();
informationList.add(Component.getInstance(
"infoImplementation1", true));
informationList.add(Component.getInstance(
"infoImplementation2", true));
informationList.add(Component.getInstance(
"infoImplementation3", true));
// ...
}
return informationList;
}
W ten sposób nie trzeba się definiować kolejnych pól.
To rozwiązanie jest tylko o połowę mniej straszne niż poprzednie.
Użycie obserwatora
Jeżeli program ma działać tak samo, niezależnie od istniejących implementacji powiadomień, najlepszym wyjściem będzie zastosowanie obserwatorów.
Aby osiągnąć zamierzony efekt, w klasie Notifications utworzę nową metodę, wywoływaną podczas tworzenia komponentu (@Create). Dzięki zastosowaniu adnotacji @RaiseEvent, będzie ona wysyłać powiadomienie o nowym zdarzeniu do wszystkich obserwatorów:
@Create
@RaiseEvent("notificationsCreated")
public void create() {
}
Prostym przykładem powiadomienia będzie informacja o aktualnej dacie i godzinie:
@Name("dateInfo")
@Scope(ScopeType.PAGE)
public class DateInfo implements Info {
@In
private List<Info> notifications;
@Observer("notificationsCreated")
public void init() {
notifications.add(this);
}
public String getText() {
return String.format(
"Now is %s.",
(new Date()).toString());
}
}
Metoda opisana adnotacją @Observer będzie wywoływana za każdym razem, gdy zostanie zgłoszone zdarzenie notificationsCreated. Spowoduje ona dodanie obiektu klasy DateInfo do wstrzykniętej listy powiadomień (notifications).
Po uruchomieniu aplikacji, na stronie powinna się pojawić informacja podobna do następującej:
- Now is Mon Aug 31 20:01:58 CEST 2009.
Ale powiadomienia mają korzystać z bazy danych, czyli muszą korzystać z menedżera encji. Dlatego też utworzę kolejną implementację interfejsu Info:
@Name("entityInfo")
@Scope(ScopeType.PAGE)
public class EntityInfo implements Info {
@In
private List<Info> notifications;
@Observer("notificationsCreated")
public void init() {
notifications.add(this);
}
@In
private EntityManager entityManager;
public String getText() {
return entityManager == null?
"Null entity manager." :
"Entity manager exists.";
}
}
Na razie ograniczę się do sprawdzenia (przy użyciu metody getText()), czy właściwy komponent został wstrzyknięty do powiadomienia. Zawartość strony przedstawia się następująco:
- Now is Mon Aug 31 20:16:36 CEST 2009.
- Null entity manager.
Ale co się stało? Dlaczego nie ma menedżera encji? Przecież wstrzyknięcie listy powiadomień (pole notifications) zadziałało bez zarzutu.
Prześledźmy działanie obserwatora:
- Zgłaszane jest zdarzenie
notificationsCreated. - Komponent
entityInfozawiera metodę obserwującą to zdarzenie, więc jego instancja ładowana jest do kontekstu zdarzenia:- następuje wstrzyknięcie zależności do komponentu,
- komponent zostaje utworzony.
- Obecna instancja (
this) komponentu zostaje dodana do listy powiadomień (linijka 9). - Obsługa zdarzenia kończy się – kontekst tego zdarzenia jest usuwany.
- Instancja klasy
EntityInfowciąż istnieje (wewnątrz listy), ale nie znajduje się już w kontekście. - W momencie wywołania metody
getText(), wszystkie wstrzykiwane pola są już puste – pojawia się informacja: Null entity manager.
Oznacza to, że komponent utworzony jako obserwator znajduje się tylko w kontekście obserwowanego zdarzenia (mimo iż został zdefiniowany jako komponent w kontekście strony – ScopeType.PAGE).
Problem powinna rozwiązać drobna zmiana w metodzie init():
@Observer("notificationsCreated")
public void init() {
notifications.add((Info) Component.getInstance(
"entityInfo", ScopeType.PAGE, true));
}
Tym razem do powiadomień dodawana jest instancja komponentu utworzona w kontekście strony, a nie – jak wcześniej – obecna.
Sprawdzam działanie strony:
- Now is Mon Aug 31 20:18:42 CEST 2009.
- Entity manager exists.
Świetnie!
Podsumowanie
Użycie obserwatora pozwala uniezależnić działanie programu od istniejących implementacji omawianego interfejsu.
W przeciwieństwie do klasycznego wzorca projektowego, mechanizm utworzony w Seam Framework pozwala na obserwację zdarzeń jedynie przy wykorzystaniu adnotacji. Nie ma potrzeby dodawania każdego obserwatora do obiektu obserwowanego.
Post scriptum
Za każdym razem, gdy szukam przykładów w Internecie, brakuje mi informacji o pakietach, z których pochodzą wykorzystywane klasy czy adnotacje. Postanowiłem, aby odtąd dołączać do każdego artykułu odpowiednią listę importów:
import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.persistence.EntityManager; import org.jboss.seam.Component; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.Create; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Observer; import org.jboss.seam.annotations.RaiseEvent; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.Unwrap;



