Fork me on GitHub

Programming Design Notes

Design Pattern - Observer

| Comments

Observer Pattern 有 2 個重要角色「主題」和「觀察者」,觀察者希望主題狀態有變更時會立即被告知。

你可以想像 Blog 上的 Email 訂閱服務(本 Blog 沒有),當你對這個 Blog 有興趣時, 你會用你的 Email 地址去訂閱服務,當有更新時你會立即收到通知,題示你有更新了,請去看看。 當你對 Blog 上千篇一律的內容感到無聊時,你亦可以取消這個訂閱,那以後有更新亦不會再通知你。

看看以下例子應該有助你明白這個模式:


公司要你開發一個即時新聞平台,用戶只需要打開程式就能夠看到即時新聞。

你一開始可能會用 Pull 的方法,每隔一段時間會主動去向主題拿取新的內容:

(NewsTopic.java) download
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class NewsTopic {

    private final Map<String, String> messages = new HashMap<String, String>();

    public Map<String, String> getMessages() {
        return messages;
    }

    public void addMessage(String message) {
        // Assign unique ID to each message for determine is it old message.
        messages.put(UUID.randomUUID().toString(), message);
    }

}
(Client.java) download
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimerTask;

public class Client extends TimerTask {

    private final Set<String> displayedIds = new HashSet<String>();

    private NewsTopic topic;

    public Client(NewsTopic topic) {
        this.topic = topic;
    }

    public void displayNews() {
        Map<String, String> messages = topic.getMessages();
        if (!displayedIds.containsAll(messages.keySet())) {
            for (Entry<String, String> entry : messages.entrySet()) {
                if (!displayedIds.contains(entry.getKey())) {
                    System.out.println(String.format(
                            "Client %s display message: %s at time: %s",
                            Integer.toHexString(this.hashCode()),
                            entry.getValue(), new Date()));
                    displayedIds.add(entry.getKey());
                }
            }
        }
    }

    @Override
    public void run() {
        displayNews();
    }

}
(Main.java) download
import java.util.Random;
import java.util.Timer;

public class Main {

    public static void main(String[] args) {

        NewsTopic topic = new NewsTopic();

        Client client1 = new Client(topic);
        Client client2 = new Client(topic);
        Client client3 = new Client(topic);

        Timer timer1 = new Timer();
        timer1.schedule(client1, 50, 3000);

        Timer timer2 = new Timer();
        timer2.schedule(client2, 50, 5000);

        Timer timer3 = new Timer();
        timer3.schedule(client3, 50, 7000);

        Random random = new Random();

        try {
            for (int i = 0; i < 3; i++) {
                topic.addMessage("News " + i);
                Thread.sleep(random.nextInt(5000));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        timer1.cancel();
        timer2.cancel();
        timer3.cancel();

    }

}
執行結果
Client 109a4c display message: News 0 at time: Fri Mar 30 00:41:23 CST 2012
Client c40c80 display message: News 0 at time: Fri Mar 30 00:41:25 CST 2012
Client c40c80 display message: News 1 at time: Fri Mar 30 00:41:25 CST 2012
Client 109a4c display message: News 2 at time: Fri Mar 30 00:41:26 CST 2012
Client 109a4c display message: News 1 at time: Fri Mar 30 00:41:26 CST 2012

每個 Client 接收到訊息的時間不一。


有客戶抱怨,股票新聞不夠實時,害他掉了錢。公司要你改善一下程式,盡量減少延遲。

現在是 Observer Pattern 大派用場的時候,因為一有更新就會主動通知觀察者,大大減低新聞延遲的問題。

你可能會問,使用舊方法但將間隔縮短不就可以了嗎? 這個方法雖然可以解決延遲的問題,但如果客戶多達 10000 人時,每一個也不停重覆去檢查資料是非常浪費系統資源, CPU 長期高工作量,會拖跨了系統的其他程式。

(Subject.java) download
public interface Subject {

    public void registerObserver(Observer observer);
    public void unregisterObserver(Observer observer);
    public void notifyObservers();

}
(Observer.java) download
public interface Observer {

    public void notify(String message);

}
(NewsTopic.java) download
import java.util.HashSet;
import java.util.Set;

public class NewsTopic implements Subject {

    private final Set<Observer> obervers = new HashSet<Observer>();
    private String message;

    public void pushMessage(String message) {
        this.message = message;
        notifyObservers();
    }

    @Override
    public void registerObserver(Observer observer) {
        obervers.add(observer);
    }

    @Override
    public void unregisterObserver(Observer observer) {
        obervers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer: obervers){
            observer.notify(message);
        }
    }

}
(Client.java) download
import java.util.Date;

public class Client implements Observer {

    @Override
    public void notify(String message) {
        System.out.println(String.format(
                "Client %s display message: %s at time: %s",
                Integer.toHexString(this.hashCode()), message, new Date()));
    }

}
(Main.java) download
import java.util.Random;

public class Main {

    public static void main(String[] args) {

        NewsTopic topic = new NewsTopic();

        Client client1 = new Client();
        Client client2 = new Client();
        Client client3 = new Client();

        topic.registerObserver(client1);
        topic.registerObserver(client2);
        topic.registerObserver(client3);

        Random random = new Random();

        try {
            for (int i = 0; i < 3; i++) {
                topic.pushMessage("News " + i);
                Thread.sleep(random.nextInt(5000));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}
執行結果
Client 10b30a7 display message: News 0 at time: Fri Mar 30 00:54:03 CST 2012
Client ca0b6 display message: News 0 at time: Fri Mar 30 00:54:03 CST 2012
Client 14318bb display message: News 0 at time: Fri Mar 30 00:54:03 CST 2012
Client 10b30a7 display message: News 1 at time: Fri Mar 30 00:54:07 CST 2012
Client ca0b6 display message: News 1 at time: Fri Mar 30 00:54:07 CST 2012
Client 14318bb display message: News 1 at time: Fri Mar 30 00:54:07 CST 2012
Client 10b30a7 display message: News 2 at time: Fri Mar 30 00:54:09 CST 2012
Client ca0b6 display message: News 2 at time: Fri Mar 30 00:54:09 CST 2012
Client 14318bb display message: News 2 at time: Fri Mar 30 00:54:09 CST 2012

現在每一次發佈訊息時,每一個客戶都同時收到,大大減低了延遲和系統負荷。


以下的圖片是描述 Observer Pattern 的 Class Diagram。(From Wiki)