前端的設計模式系列 - 觀察者模式

代碼也寫了幾年了,設計模式處於看了忘,忘了看的狀態,最近對設計模式有了點感覺,索性就再學習總結下吧。

大部分講設計模式的文章都是使用的 JavaC++ 這樣的以類爲基礎的靜態類型語言,作爲前端開發者,js 這門基於原型的動態語言,函數成爲了一等公民,在實現一些設計模式上稍顯不同,甚至簡單到不像使用了設計模式,有時候也會產生些困惑。

下面按照「場景」-「設計模式定義」- 「代碼實現」-「總」的順序來總結一下,如有不當之處,歡迎交流討論。

場景

假設我們在開發一款外賣網站,進入網站的時候,第一步需要去請求後端接口得到用戶的常用外賣地址。然後再去請求其他接口、渲染頁面。如果什麼都不考慮可能會直接這樣寫:

// getAddress 異步請求
// 頁面裏有三個模塊 A,B,C 需要拿到地址後再進行下一步
// A、B、C 三個模塊都是不同人寫的,提供了不同的方法供我們調用

getAddress().then(res ={
  const address = res.address;
  A.update(address)
  B.next(address)
  C.change(address)
})

此時頁面裏多了一個模塊 D ,同樣需要拿到地址後進行下一步操作,我們只好去翻請求地址的代碼把 D 模塊的調用補上。

// getAddress 異步請求
// 頁面裏有三個模塊 A,B,C 需要拿到地址後再進行下一步
// A、B、C 三個模塊都是不同人寫的,提供了不同的方法供我們調用

getAddress().then(res ={
  const address = res.address;
  A.update(address)
  B.next(address)
  C.change(address)
  D.init(address)
})

可以看到各個模塊和獲取地址模塊耦合嚴重,ABC 模塊有變化或者有新增模塊,都需要深入到獲取地址的代碼去修改,一不小心可能就改出問題了。

此時就需要觀察者模式了。

設計模式定義

可以看下 維基百科的介紹:

The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

很好理解的一個設計模式,有一個 subject 對象,然後有很多 observers 觀察者對象,當 subject 對象有變化的時候去通知 observer 對象即可。

再看一下 UML 圖和時序圖:

image-20220127110751274

每一個觀察者都實現了 update 方法,並且調用 Subject 對象的 attach 方法訂閱變化。當 Subject 變化時,調用 Observerupdate 方法去通知觀察者。

先用 java 寫一個簡單的例子:

公衆號文章可以看作是 Subject ,會不定期更新。然後每一個用戶都是一個 Observer ,訂閱公衆號,當更新的時候就可以第一時間收到消息。

import java.util.ArrayList;

interface Observer {
    public void update();
}
// 提取 Subject 的公共部分
abstract class Subject {
    private ArrayList<Observer> list = new ArrayList<Observer>();
    public void attach(Observer observer){
        list.add(observer);
    }
    public void detach(Observer observer){
        list.remove(observer);
    }
    public void notifyObserver(){
        for(Observer observer : list){
            observer.update();
        }
    }
}
// 具體的公衆號,提供寫文章和得到文章
class WindLiang extends Subject {
    private String post;

    public void writePost(String p) {
        post = p;
    }

    public String getPost() {
        return post;
    }
}

// 小明
class XiaoMing implements Observer {
    private WindLiang subject;

    XiaoMing(WindLiang sub) {
        subject = sub;
    }
    @Override
    public void update(){
        String post = subject.getPost();
        System.out.println("我收到了" + post + " 並且點了個贊");
    }
}

// 小楊
class XiaoYang implements Observer {
    private WindLiang subject;

    XiaoYang(WindLiang sub) {
        subject = sub;
    }
    @Override
    public void update(){
        String post = subject.getPost();
        System.out.println("我收到了" + post + " 並且轉發了");
    }
}

// 小剛
class XiaoGang implements Observer {
    private WindLiang subject;

    XiaoGang(WindLiang sub) {
        subject = sub;
    }
    @Override
    public void update(){
        String post = subject.getPost();
        System.out.println("我收到了" + post + " 並且收藏");
    }
}


public class Main {
    public static void main(String[] args) {
        WindLiang windliang = new WindLiang(); // Subject
        XiaoMing xiaoMing = new XiaoMing(windliang);
        XiaoYang xiaoYang = new XiaoYang(windliang);
        XiaoGang xiaoGang = new XiaoGang(windliang);
      
        // 添加觀察者
        windliang.attach(xiaoMing);
        windliang.attach(xiaoYang);
        windliang.attach(xiaoGang);

        windliang.writePost("新文章-觀察者模式,balabala"); // 更新文章
        windliang.notifyObserver(); // 通知觀察者
    }
}

輸出結果如下:

上邊的實現主要是爲了符合最原始的定義,調用 update 的時候沒有傳參。如果觀察者需要的參數是一致的,其實這裏也可以直接把更新後的數據傳過去,這樣觀察者就不需要向上邊一樣再去調用 subject.getPost() 手動拿更新後的數據了。

這兩種不同的方式前者叫做拉 (pull) 模式,就是收到 Subject 的通知後,通過內部的 Subject  對象調用相應的方法去拿到需要的數據。

後者叫做推 (push) 模式,Subject 更新的時候就將數據推給觀察者,觀察者直接使用即可。

下邊用 js 改寫爲推模式:

const WindLiang = () ={
    const list = [];
    let post = "還沒更新";
    return {
        attach(update) {
            list.push(update);
        },
        detach(update) {
            let findIndex = -1;
            for (let i = 0; i < list.length; i++) {
                if (list[i] === update) {
                    findIndex = i;
                    break;
                }
            }
            if (findIndex !== -1) {
                list.splice(findIndex, 1);
            }
        },
        notifyObserver() {
            for (let i = 0; i < list.length; i++) {
                list[i](post);
            }
        },
        writePost(p) {
            post = p;
        },
    };
};

const XiaoMing = {
    update(post){
        console.log("我收到了" + post + " 並且點了個贊");
    }
}

const XiaoYang = {
    update(post){
        console.log("我收到了" + post + " 並且轉發了");
    }
}

const XiaoGang = {
    update(post){
        console.log("我收到了" + post + " 並且收藏");
    }
}


windliang = WindLiang();

windliang.attach(XiaoMing.update)
windliang.attach(XiaoYang.update)
windliang.attach(XiaoGang.update)

windliang.writePost("新文章-觀察者模式,balabala")
windliang.notifyObserver()

js 中,我們可以直接將 update 方法傳給 Subject ,同時採取推模式,調用 update 的時候直接將數據傳給觀察者,看起來會簡潔很多。

代碼實現

回到開頭的場景,我們可以利用觀察者模式將獲取地址後的一系列後續操作解耦出來。

// 頁面裏有三個模塊 A,B,C 需要拿到地址後再進行下一步
// A、B、C 三個模塊都是不同人寫的,提供了不同的方法供我們調用
const observers = []
// 註冊觀察者
observers.push(A.update)
observers.push(B.next)
obervers.push(C.change)



// getAddress 異步請求
getAddress().then(res ={
  const address = res.address;
  observers.forEach(update => update(address))
})

通過觀察者模式我們將獲取地址後的操作解耦了出來,未來有新增模塊只需要註冊觀察者即可。

getAddress 很複雜的時候,通過觀察者模式會使得未來的改動變得清晰,不會影響到 getAddress 的邏輯。

必要的話也可以把 observers 抽離到一個新的文件作爲一個新模塊,防止讓一個文件變得過於臃腫。

觀察者模式比較好理解,通過抽象出一個 Subject 和多個觀察者,減輕了它們之間的過度耦合。再說簡單點就是利用回調函數,異步完成後調用傳入的回調即可。但上邊寫的觀察者模式還是有一些缺點:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/a79WUhe-hcMAiEsiz6KkwQ