1.1 Definicja
Wzorzec projektowy obserwator definiuje zależność jeden do wielu między obiektami, w taki sposób, że gdy jeden obiekt zmieni swój stan, wszystkie obiekty zależne będą o tym powiadomione automatycznie. Co ważne wzorzec obserwator zapewnia utworzenie takiej struktury obiektów, w której obiekty obserwowane są luźno powiązane ze swoimi obiektami obserwującymi. Obiekt, który jest obserwowany nazywane jest Podmiotem natomiast obiekty obserwujące to obserwatorzy lub słuchacze.
1.2 Wzorzec obserwator – przykład zastosowania
Jeśli chodzi o wykorzystanie wzorca obserwator w prawdziwych projektach to można wymienić chociażby dowolny UI wykorzystujący myszkę czy klawiaturę. Wszystkie zdarzenia klawiatury są obsługiwane przez obiekty nasłuchujące (obserwatorów) i odpowiednie funkcje. Kiedy użytkownik naciska klawisz klawiatury, przypisana do tego zdarzenia funkcja jest wywoływana ze wszystkimi danymi kontekstowymi przekazanymi do niej jako argument metody.
2.0 Architektura wzorca obserwator.
2.1 Części składowe
Wzorzec obserwator posiada 4 główne części składowe:
- Subject – interfejs lub klasa abstrakcyjna definiująca operacje służące do dodawania i usuwania obserwatorów do podmiotu.
- ConcreteSubject – to konkretna klasa Podmiotu. Zarządza stanem obiektu, jeśli stan obiektu się zmieni, obiekt Podmiot automatycznie powiadamia o tym obiekty obserwujące.
- Observer – interfejs lub klasa abstrakcyjna definiująca operacje, które mają być użyte do powiadomienia tego obiektu.
- ConcreteObserwer – implementacja konkretnego obserwatora.
3.0 Przykład implementacji
Napiszemy prostą symulację, która wysyłała będzie zmiany statusu naszego samopoczucia. Tradycyjnie zaczniemy od prostej implementacji bez wykorzystania wzorca obserwator. Utwórzmy zatem klasę User, która zawierać będzie dwa pola prywatne: myStatus oraz userID, do tego konstruktor przyjmujący nasze pole myStatus i userID, na koniec dodamy gettery i settery.
public class User {
private MyStatus myStatus;
private int userID;
public User(int userID, MyStatus myStatus) {
this.myStatus = myStatus;
this.userID = userID;
}
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
public MyStatus getMyStatus() {
return myStatus;
}
public void setMyStatus(MyStatus myStatus) {
this.myStatus = myStatus;
}
}
Teraz tworzymy klasę enum MyStatus z typami naszego samopoczucia.
public enum MyStatus {
WESOLY,
SMUTNY,
OBOJETNY
}
Przyszedł czas na klasę Status obsługującą powiadomienie uzytkowników o zmianie statusu naszego usera.
public class Status {
public void updateUserStatus(User user) {
System.out.println("Uzytkownik o ID: " + user.getUserID() + ", zmienil status na: " + user.getMyStatus());
}
}
I wreszcie nasza klasa Main gdzie utworzymy użytkownika i powiadomimy innych o zmianie naszego statusu, poprzez wywołanie metody updateUserStatus z klasy Status.
public class Main {
public static void main(String[] args) {
User user = new User(1, MyStatus.WESOLY);
Status status = new Status();
status.updateUserStatus(user);
}
}
Jak widać nie jest to skomplikowana implementacja ale przez to posiada główną wadę – nie jest łatwo podatna na rozbudowę. Jeżeli chcielibyśmy dodawać kolejne kanały komunikacyjne jak np. SMS, Whatsapp itd, kod mógłby szybko stać się zagmatwany. Z pomocą przychodzi nam wzorzec Obserwator, który poprzez zastosowanie abstrakcji oraz podejścia jeden do wielu rozwiąże nasz główny problem.
Zobaczmy zatem implementację z wykorzystanie wzorca Obserwator
Zgodnie ze schematem UML, zaczniemy od napisania interfejsów. Na pierwszy ogień zobaczmy jak wyglądać będzie interfejs Observable czyli ten który jest obserwowany. Jest to interfejs posiadający trzy metody (metody są oczywiście bez ciała) rejestrację obserwatorów, wyrejestrowanie obserwatorów oraz powiadomienie obserwatorów.
public interface Observable {
void registerObserver(Observer observer);
void unregisterObserver(Observer observer);
void notifyObservers();
}
Teraz pora na interfejs Obserwatora, mamy tutaj prosta implementację z jedną metodą aktualizującą obiekt user:
public interface Observer {
void update(User user);
}
Przejdźmy zatem do implementacji klasy User która implementować będzie interfejs Observable. Zobaczmy pierwszą różnicę w porównaniu do naszej klasy User bez wzorca Obserwator. Deklarujemy kolekcję obiektów Obserwator, kolekcja HashSet zapewnia nam unikalność dodawanych obiektów. Następnie nadpisujemy metody z interfejsu. O ile metody register i unregister Observer nie wymagają komentarza o tyle metoda notifyObserver potrzebuje krótkiego wyjaśnienia. Otóż w metodzie tej wykonujemy pętle przez wszystkie obiekty w naszej kolekcji obserwatorów i na każdym wykonujemy metodę update. Dołożyliśmy jeszcze metodę chaneUSerStatus, w której zmieniamy status usera i od razu powiadamiamy naszych obserwatorów.
public class User implements Observable {
private MyStatus myStatus;
private String name;
private Set<Observer> registeredObservers = new HashSet<>();
public User(String name, MyStatus myStatus) {
this.myStatus = myStatus;
this.name = name;
}
@Override
public void registerObserver(Observer observer) {
registeredObservers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
registeredObservers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : registeredObservers){
observer.update(this);
}
}
public void changeUserStatus(MyStatus myStatus){
setMyStatus(myStatus);
notifyObservers();
}
public String getName() {
return name;
}
public void setUserName(int userID) {
this.name = name;
}
public MyStatus getMyStatus() {
return myStatus;
}
public void setMyStatus(MyStatus myStatus) {
this.myStatus = myStatus;
}
}
Dodajmy teraz nasz pierwszy kanał komunikacyjny – SMS czyli klasę naszego Obserwatora, która implementuje interfejs Observer.
public class SMS implements Observer {
@Override
public void update(User user) {
System.out.println("SMS: Uzytkownik o imieniu " + user.getName() + ", zmienil status na: " + user.getMyStatus());
}
}
To wszystko, czas na maina:
public class Main {
public static void main(String[] args) {
User user = new User("Mariusz", MyStatus.WESOLY);
SMS sms = new SMS();
Email email = new Email();
user.registerObserver(sms);
user.registerObserver(email);
user.notifyObservers();
System.out.println("-------------------");
user.changeUserStatus(MyStatus.OBOJETNY);
}
}
Zobaczmy wynik działania takiego programu:
Email: Uzytkownik o imieniu Mariusz, zmienil status na: WESOLY
SMS: Uzytkownik o imieniu Mariusz, zmienil status na: WESOLY
-------------------
Email: Uzytkownik o imieniu Mariusz, zmienil status na: OBOJETNY
SMS: Uzytkownik o imieniu Mariusz, zmienil status na: OBOJETNY
4.0 Podsumowanie
Wzorzec Obserwator definiuje pomiędzy obiektami relację jeden-do-wielu w taki sposób, że kiedy wybrany obiekt zmienia swój stan, to wszystkie jego obiekty zależne zostają o tym powiadomione i automatycznie zaktualizowane. Wzorzec obserwator zapewnia utworzenie takiej struktury obiektów, w której obiekty obserwowane są luźno powiązane z swoimi obiektami obserwującymi.