Observer

January 5, 2022

你打算開發多人連線程式,每個連線後的客戶端,都會建立一個 Client 物件封裝客戶端資訊, 然後加入 ClientQueue 管理:

record Client(String ip, String name) {}

class ClientQueue {
    private List<Client> clients = new LinkedList<>();
    
    void add(Client client) {
        clients.add(client);
    }

    void remove(Client client) {
        clients.remove(client);
    }
    ....
}

現在你想在 ClientQueue 新增、移除 Client 實例時執行一些任務,於是你在 addremove 增加了相關的程式碼,之後別的同事也想在新增、移除 Client 實例時執行一些任務,於是他也在 addremove 增加了相關的程式碼,然後又有人想在新增、移除 Client 實例時執行一些任務…唉…等等…

你在看我嗎?

看來很多人都會 ClientQueue 新增、移除 Client 的時機感興趣,只不過做的事情各不相同,那就跟 ClientQueue 登記吧!ClientQueue 會在新增、移除發生時通知你,想做什麼就你家的事!

既然要能通知,就要有個統一的聯絡方式:


import java.util.EventObject;

interface ClientListener {
    void clientAdded(EventObject event);
    void clientRemoved(EventObject event);
}

然後 ClientQueue 提供登記與移除通知的方法:

class ClientQueue {
    private List<Client> clients = new LinkedList<>();
    private List<ClientListener> listeners = new LinkedList<>();
            
    void addClientListener(ClientListener listener) {
        listeners.add(listener);
    }
    void removeClientListener(ClientListener listener) {
        listeners.remove(listener);
    }
    
    void notifyAdded(Client client) {
        for(var listener : listeners) {
            listener.clientAdded(new EventObject(client));
        }
    }
    void notifyRemoved(Client client) {
        for(var listener : listeners) {
            listener.clientRemoved(new EventObject(client));
        }
    }
    
    void add(Client client) {
        clients.add(client);
        notifyAdded(client);
    }
    void remove(Client client) {
        clients.remove(client);
        notifyRemoved(client);
    }
	...
}

ClientQueueClient 新增、移除有興趣的,可以實作 ClientListener,向 ClientQueue 註冊:

class ClientLogger implements ClientListener {
    public void clientAdded(EventObject event) {
		var client = (Client) event.getSource();
        out.println(client.ip() + " added...");
    }
    public void clientRemoved(EventObject event) {
		var client = (Client) event.getSource();
        out.println(client.ip() + " removed...");
    }
}

public class Main {
    public static void main(String[] args) {
        var queue = new ClientQueue();
        queue.addClientListener(new ClientLogger());
        var c1 = new Client("127.0.0.1", "Justin");
        var c2 = new Client("192.168.0.2", "Monica");
        queue.add(c1);
        queue.add(c2);
        queue.remove(c1);
        queue.remove(c2);
    }
}

被觀察的對象

在 Gof 稱這類實現為 Observer 模式,你可能會說,這不是就是常見的事件處理嗎?沒錯!事件處理基本是 Observer 模式的應用之一,只不過有時候感興趣的時機,不見得是某個物件的狀態變化,可能是某個模組甚至是應用程式生命週期的變化。

就這邊看到的範例來說,其實一開始你想在 ClientQueue 新增、移除 Client 實例時執行一些任務,就在 addremove 增加了相關程式碼,這個動作並沒有錯,重點在於之後,持續有人對 addremove 感興趣的這個事實引發了重構的動機,發覺了 ClientQueue 可以負責通知,各自實作由感興趣的人自己處理的想法。

如果應用程式本身負有某種生命週期管理的職責,在一開始規劃時,有經驗的開發者可能就會想在某些生命週期變化時,提供事件處理機制,因而一開始就定義各種傾聽器介面,實現 Observer 的概念;然而,有時候一個物件本身經常被觀察這件事,並不是那麼容易察覺。

因為你可能一開始想在某時機做什麼,就直接在物件中新增了一些程式,若干時日後,又想做個什麼,然後很久之後, 又加入了什麼,持續在某年某月某一書為了某時機,直接加入某些程式碼這件事,也可能是你的同事或者你的接班者,物件的職責可能需要重構,可能就是個被觀察者,只是遲遲沒有被察覺罷了。