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

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

大部分講設計模式的文章都是使用的 Java、C++ 這樣的以類爲基礎的靜態類型語言,作爲前端開發者,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)
})

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

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

設計模式定義

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

★

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 變化時,調用 Observer 的 update 方法去通知觀察者。

先用 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