Listopad 21, 2018

Mam Cię na oku – wzorzec projektowy Obserwator

Obserwator

Wzorzec projektowy obserwator umożliwia reakcję w obiektach (obserwatorach) na zmiany zachodzące w obiekcie obserwowanym. I w sumie mógłbym zostawić cię z taką definicją i zakończyć ten artykuł, ale to mijałoby się z celem. Choć zasada działania obserwatora wydaje się prosta to na samym początku obserwator może wydawać się problematyczny do zaimplementowania.

W tym artykule pokażę Ci na praktycznym przykładzie jak zaimplementować wzorzec projektowy obserwator – no to do dzieła!

Zasada działania

Omówmy sobie dogłębnie działanie obserwatora. W owym wzorcu projektowym wyróżniamy dwa byty:

  • observer (obserwator) – czyli obiekt, który obserwuje konkretny obiekt i reaguje odpowiednio na zmiany w nim zachodzące
  • observable (obserwowany) – obiekt, który jest obserwowany przez wielu obserwatorów

Zanim zajmiemy się kodem wyobraźmy sobię sytuację, gdzie owy wzorzec mógłby się przydać.

Przypuśćmy, że mamy kanał na Youtube, który jest subskrybowany przez pewnych subskrybentów. Mogą oni zaznaczyć chęć otrzymywania powiadomień w przypadku, gdy pojawi się nowy film na kanale.

Subskrybenci są naszymi obserwatorami – obserwują oni kanał Youtube – w przypadku, gdy zmieni się stan obiektu obserwowanego (zostanie dodany nowy film) to subskrybenci powinni automatycznie się o tym dowiedzieć.

Mogą się dowiedzieć przy pomocy powiadomienia PUSH lub e-maila. Bez różnicy jak, chodzi nam o samą zasadę działania.

Konstrukcja

Tak jak wspomniałem wyróżniamy dwa byty: observer, observable – warto dla nich stworzyć osobne interfejsy. Musimy mieć w końcu pewność, że oba byty zawierają potrzebne nam metody podczas korzystania ze wzorca projektowego.

Observable powinien zawierać metody umożliwiające mu:

  • dodanie nowego obserwatora (subskrypcja kanału),
  • usunięcie istniejącego obserwatora (odsubskrybowanie kanału),
  • poinformowanie obserwatorów o wystąpieniu zmiany (pojawił się nowy film na kanale).

Interfejs takiego bytu będzie wyglądał tak:

public interface Observable {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

W interfejsie jest już używany typ Observer, już do niego nawiązuje.

Observer powinien zawierać metodę, która jest uruchamiana po wystąpieniu zmiany w obiekcie obserwującym – zazwyczaj nazywa się update:

  • aktualizuj (powiadomienie o nowym kanale na Youtube)

Interfejs obserwatora prezentuje się tak:

public interface Observer {
    void update();
}

Blog subscribers – Observer

Mając stworzone już potrzebne interfejsy dla naszych dwóch bytów – obserwator i obserwowany – możemy przejść do implementacji problemu.

Opowiadałem wcześniej o subskrypcji kanału na Youtube, skoro tamten przykład został już omówiony od strony teoretycznej to weźmy inny przykład dla części praktycznej.

Przypuśćmy, że mamy aplikację, do której można dodawać blogi. Użytkownicy mogą subskrybować interesujące ich blogi i otrzymywać powiadomienia: push, email oraz sms – gdy tylko pojawi się nowy artykuł na jednym z zasubskrybowanych blogów.

Na początek mamy interfejs, który definiuje naszego subskrybenta bloga:

public interface Subscriber {
    boolean isEmailNotificationEnabled();
    boolean isPushNotificationEnabled();
    boolean isSmsNotificationEnabled();
    void setPushNotificationEnable(boolean state);
    void setEmailNotificationEnable(boolean state);
    void setSmsNotificationEnable(boolean state);
    String getName();
    String getEmail();
    String getPhoneNumber();
}

Następnie stworzymy sobie model dla subskrybenta – będziemy tam przechowywać imię użytkownika, email, numer telefonu oraz ustawione preferencje powiadomień.

public class SubscriberData implements Subscriber {
    private final String name;
    private final String email;
    private final String phoneNumber;
    private boolean isEmailNotificationEnable = true;
    private boolean isSmsNotificationEnable = true;
    private boolean isPushNotificationEnable = true;


    public SubscriberData(String name, String email, String phoneNumber) {
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getEmail() {
        return email;
    }

    @Override
    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setEmailNotificationEnable(boolean emailNotificationEnable) {
        isEmailNotificationEnable = emailNotificationEnable;
    }

    public void setSmsNotificationEnable(boolean smsNotificationEnable) {
        isSmsNotificationEnable = smsNotificationEnable;
    }

    public void setPushNotificationEnable(boolean pushNotificationEnable) {
        isPushNotificationEnable = pushNotificationEnable;
    }

    @Override
    public boolean isEmailNotificationEnabled() {
        return isEmailNotificationEnable;
    }

    @Override
    public boolean isPushNotificationEnabled() {
        return isPushNotificationEnable;
    }

    @Override
    public boolean isSmsNotificationEnabled() {
        return isSmsNotificationEnable;
    }
}

Jak już wiemy subskrybent będzie obserwatorem zasubskrybowanego bloga, stwórzmy dodatkowy interfejs, który będzie rozszerzał Observer – zawrzemy w nim metody odpowiedzialne za różny sposoby komunikacji z subskrybentem.

public interface SubscriberObserver extends Observer {
    void sendPushNotification();
    void sendEmailNotification();
    void sendSmsNotification();
}

Na koniec stworzymy sobie w końcu subskrybenta bloga – będzie on zawierał ilość nieprzeczytanych artykułów na zasubskrybowanych blogach oraz podstawowe informacje o subskrybencie (Subscriber).

public class BlogSubscriber implements SubscriberObserver {
    private final static String RESULT_INFORMATION = "[%s] %s, you have %d unread articles";
    private final Subscriber subscriber;
    private int amountUnreadArticles = 0;

    public BlogSubscriber(Subscriber subscriber) {
        this.subscriber=subscriber;
    }

    @Override
    public void update() {
    }

    @Override
    public void sendPushNotification() {
    }

    @Override
    public void sendEmailNotification() {
    }

    @Override
    public void sendSmsNotification() {
    }
}

Zacznijmy od początku, ta linijka:

private final static String RESULT_INFORMATION = "[%s] %s, you have %d unread articles";

Będzie odpowiadała za format i wiadomość wyświetlanej informacji subskrybentowi po pojawieniu się nowego artykułu, wrócimy do niej podczas implementacji metod send.

Najważniejszym dla nas punktem wejściowym obserwatora jest metoda update(), która jak już wiesz jest uruchamiana po zmianie stanu obiektu obserwowanego (w naszym przypadku bloga).

Co możemy zdefiniować jako zmiana stanu bloga?

Dla mnie będzie to dodanie nowego artykułu, w takim razie na pewno zwiększy się ilość nieprzeczytanych artykułów.

@Override
public void update() {
    amountUnreadArticles++;
}

Oraz wyślemy powiadomienie na wszystkie zadeklarowane przez subksrybenta kanały komunikacji:

@Override
public void update() {
    amountUnreadArticles++;
    sendEmailNotification();
    sendPushNotification();
    sendSmsNotification();
}

Zdefiniujmy sobie na początku kanały komunikacji jako enum, które zostaną użyte przy powiadomieniu:

public enum NotificationTags {
    EMAIL,
    SMS,
    PUSH
}

Oczywiście przed wysłaniem musimy sprawdzić czy subskrybent chce otrzymywać owe powiadomienia: 😉

@Override
public void sendPushNotification() {
    if (subscriber.isPushNotificationEnabled()) {
        System.out.println(String.format(RESULT_INFORMATION, NotificationTags.PUSH, subscriber.getName(), amountUnreadArticles));
    }
}

@Override
public void sendEmailNotification() {
    if (subscriber.isEmailNotificationEnabled()) {
        System.out.println(String.format(RESULT_INFORMATION, NotificationTags.EMAIL, subscriber.getName(), amountUnreadArticles));
    }
}

@Override
public void sendSmsNotification() {
    if (subscriber.isSmsNotificationEnabled()) {
        System.out.println(String.format(RESULT_INFORMATION, NotificationTags.SMS, subscriber.getName(), amountUnreadArticles));
    }
}

I to na tyle implementacji subskrybenta bloga, czas przejść do obiektu obserwowanego czyli bloga.

Bloga – Observable

Czas na implementację obiektu, który będzie obserwowany – w naszym przypadku dokładnie subskrybowany. 😉

Blog oczywiście musi implementować interfejs Observable:

public class Blog implements Observable {
    private final String blogName;

    public Blog(String blogName) {
        this.blogName = blogName;
    }

    @Override
    public void addObserver(Observer observer) {
    }

    @Override
    public void removeObserver(Observer observer) {
    }

    @Override
    public void notifyObservers() {
    }

}

Blog, aby mieć możliwość poinformowania subskrybentów o nowym artykule musi posiadać listę obserwatorów.

List<Observer> blogSubscribers = new LinkedList<>();

Podczas subskrybcji bloga dodajemy obserwatora do listy:

@Override
public void addObserver(Observer observer) {
    blogSubscribers.add(observer);
}

Umożliwimy usunięcie subskrypcji bloga:

@Override
public void removeObserver(Observer observer) {
    blogSubscribers.remove(observer);
}

Na koniec musimy mieć metodę odpowiedzialną za poinformowanie wszystkich subskrybentów o zmianie stanu obiektu obserwowanego – czyli dodaniu nowego artykułu na bloga. 😉

@Override
public void notifyObservers() {
    for(Observer observer:blogSubscribers) {
        observer.update();
    }
}

Można to wykonać przy użyciu zwykłej pętli foreach lub przy pomocy forEach z Java 8.

@Override
public void notifyObservers() {
    blogSubscribers.forEach(Observer::update);
}

Dobrze, mamy już możliwość informowania subskrybentów o zmianie stanu, ale nie mamy żadnej metody odpowiedzialnej za zmianę stanu.

Dodajmy metodę, która będzie odpowiedzialna za publikację artykułu na blogu – po publikacji wyświetlimy odpowiednią informację (czysto informacyjnie, abyśmy mogli zobaczyć przebieg akcji) i poinformujemy o nowym artykule obserwatorów.

public void publishNewArticle(String articleName) {
    System.out.println(String.format("Publish new article on blog %s about %s", blogName, articleName));
    notifyObservers();
}

Mając tak stworzoną całą klasę bloga:

import blog.subscriber.Observable;
import blog.subscriber.Observer;

import java.util.LinkedList;
import java.util.List;

public class Blog implements Observable {
    List<Observer> blogSubscribers = new LinkedList<>();
    private final String blogName;

    public Blog(String blogName) {
        this.blogName = blogName;
    }

    @Override
    public void addObserver(Observer observer) {
        blogSubscribers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        blogSubscribers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        blogSubscribers.forEach(Observer::update);
    }

    public void publishNewArticle(String articleName) {
        System.out.println(String.format("Publish new article on blog %s about %s", blogName, articleName));
        notifyObservers();
    }
}

Możemy przejść do prostych testów w klasie Main.

Show must go on!

Na początku stwórzmy sobie dwa blogi:

import blog.Blog;
import blog.subscriber.BlogSubscriber;
import blog.subscriber.SubscriberData;

public class Main {
    public static void main(String[] args) {
        Blog firstBlog = new Blog("1024kb.pl");
        Blog secondBlog = new Blog("google.pl");
    }
}

Oraz dwóch subskrybentów:

import blog.Blog;
import blog.subscriber.BlogSubscriber;
import blog.subscriber.Subscriber;
import blog.subscriber.SubscriberData;

public class Main {
    public static void main(String[] args) {
        Blog firstBlog = new Blog("1024kb.pl");
        Blog secondBlog = new Blog("google.pl");

        Subscriber meSubsriber = new SubscriberData("Kamil", "maniaq@1024kb.pl", "554-333-222");
        Subscriber pabloSubsriber = new SubscriberData("Pablo", "pablo@1024kb.pl", "444-221-222");
        
        BlogSubscriber me = new BlogSubscriber(meSubsriber);
        BlogSubscriber pablo = new BlogSubscriber(pabloSubsriber);
    }
}

Przypuśćmy, że ja nie chcę otrzymywać powiadomień  Email, a Pablo nie chce powiadomień push oraz SMS:

pabloSubsriber.setPushNotificationEnable(false);
pabloSubsriber.setSmsNotificationEnable(false);
meSubsriber.setEmailNotificationEnable(false);

Bloga 1024kb.pl będe subskrybował ja oraz pablo:

firstBlog.addObserver(me);
firstBlog.addObserver(pablo);

Zaś google.pl tylko pablo:

secondBlog.addObserver(pablo);

Niech oba blogi opublikują następujące artykuły:

firstBlog.publishNewArticle("Interfejsy w Javie");
firstBlog.publishNewArticle("Użycie HashMapy");
secondBlog.publishNewArticle("Nowy algorytm SEO");

Cała klasa Main wygląda aktualnie tak:

import blog.Blog;
import blog.subscriber.BlogSubscriber;
import blog.subscriber.Subscriber;
import blog.subscriber.SubscriberData;

public class Main {
    public static void main(String[] args) {
        Blog firstBlog = new Blog("1024kb.pl");
        Blog secondBlog = new Blog("google.pl");

        Subscriber meSubsriber = new SubscriberData("Kamil", "maniaq@1024kb.pl", "554-333-222");
        Subscriber pabloSubsriber = new SubscriberData("Pablo", "pablo@1024kb.pl", "444-221-222");

        BlogSubscriber me = new BlogSubscriber(meSubsriber);
        BlogSubscriber pablo = new BlogSubscriber(pabloSubsriber);

        pabloSubsriber.setPushNotificationEnable(false);
        pabloSubsriber.setSmsNotificationEnable(false);
        meSubsriber.setEmailNotificationEnable(false);


        firstBlog.addObserver(me);
        firstBlog.addObserver(pablo);

        secondBlog.addObserver(pablo);

        firstBlog.publishNewArticle("Interfejsy w Javie");
        firstBlog.publishNewArticle("Użycie HashMapy");
        secondBlog.publishNewArticle("Nowy algorytm SEO");
    }
}

Uruchommy program i przeanalizujmy jego działanie:

Publish new article on blog 1024kb.pl about Interfejsy w Javie
[PUSH] Kamil, you have 1 unread articles
[SMS] Kamil, you have 1 unread articles
[EMAIL] Pablo, you have 1 unread articles

Po pojawieniu się artykułu na pierwszym blogu subskrybent Kamil otrzymał powiadomienia push oraz sms, pablo zaś tylko email – tak jak ustawili to w ustawieniach – o tutaj: 😉

pabloSubsriber.setPushNotificationEnable(false);
pabloSubsriber.setSmsNotificationEnable(false);
meSubsriber.setEmailNotificationEnable(false);
Publish new article on blog 1024kb.pl about Użycie HashMapy
[PUSH] Kamil, you have 2 unread articles
[SMS] Kamil, you have 2 unread articles
[EMAIL] Pablo, you have 2 unread articles

Po pojawieniu się drugiego artykułu na pierwszym blogu powiadomienia zostały wysłane na te same kanały komunikacyjne, ale również zwiększyła się ilość nieprzeczytanych artykułów przez subskrybentów.

Publish new article on blog google.pl about Nowy algorytm SEO
[EMAIL] Pablo, you have 3 unread articles

Po pojawieniu się artykułu na drugim blogu, który jest subskrybowany tylko przez pablo – został tylko on poinformowany, dodatkowo tylko kanałem email tak jak sobie to ustawił. 😉

Podsumowanie

Jak widzisz wzorzec projektowy obserwator przydaję się w przypadku, gdy dana pula obiektów powinna reagować na zmiany konkretnego obserwowanego obiektu.

Najprostszym przykładem wykorzystania wzorca jest na pewno subskrypcja – czy to pewnego kanału lub bloga – lub dołączenie do pewnej grupy np. na Facebooku.

Z pewnością wzorzec może być wykorzystywany w innych przypadku, czy możesz podać przykład takiego zastosowania?

Podziel się przykładowym zastosowaniem obserwatora w komentarzu, czekam!

Kod z artykułu możesz znaleźć tutaj.