設計模式大冒險第一關:觀察者模式

dreamapplehappy發表於2020-10-11

封面圖

最近把之前學習過的這些設計模式又再次溫習了一下,覺得還是有很多收穫的。確實有了溫故知新的感覺,所以準備在每個設計模式複習完之後都能夠寫一篇關於這個設計模式的文章,這樣會讓自己能夠加深對這個設計模式的理解;也能夠跟大家一起來探討一下。

今天我們來一起學習一下觀察者模式,剛開始我們不需要知道觀察者模式的定義是什麼,這些我們到後面再去了解。我想先帶著大家從生活中的一個小事例開始。從生活中熟悉的事情入手,會讓我們更快速的理解這個模式的用途

生活中的小例子

相信大家都關注過一些公眾號,那麼對於一個公眾號來說,如果有新的文章釋出的話;那麼所有關注這個公眾號的使用者都會收到更新的通知,如果一個使用者沒有關注或者關注後又取消了關注,那麼這個使用者就不會收到該公眾號更新的通知。相信這個場景大家都很熟悉吧。那麼如果我們把這個過程抽象出來,用程式碼來實現的話,你會怎麼處理呢?不妨現在停下來思考一下。

通過上面的描述,我們知道這是一個一對多的關係。也就是一個公眾號對應著許多關注這個公眾號的使用者。

關注公眾號

那麼對於這個公眾號來說,它的內部需要有一個列表記錄著關注這個公眾號的使用者,一旦公眾號有了新的內容。那麼對於公眾號來說,它會遍歷這個列表。然後給列表中的每一個使用者傳送一個內容跟新的通知。我們可以通過程式碼來表示這個過程:

// 使用者
const user = {
    update() {
        console.log('公眾號更新了新的內容');
    },
};

// 公眾號
const officialAccount = {
    // 關注當前公眾號的使用者列表
    followList: [user],
    // 公眾號更新時候呼叫的通知函式
    notify() {
        const len = this.followList.length;
        if (len > 0) {
            // 通知已關注該公眾號的每個使用者,有內容更新
            for (let user of this.followList) {
                user.update();
            }
        }
    },
};

// 公眾號有新內容更新
officialAccount.notify();

執行的結果如下:

公眾號更新了新的內容

上面的程式碼能夠簡單的表示,當公眾號的內容發生了更新的時候,去通知關注該公眾號的使用者的過程。但是這個實現是很簡陋的,還缺少一些內容。我們接下來把這些缺少的過程補充完整。對於公眾號來說,還需要可以新增新的關注的使用者,移除不再關注的使用者,獲取關注公眾號的使用者總數等。我們來實現一下上面的過程:

// 公眾號
const officialAccount = {
    // 關注當前公眾號的使用者列表
    followList: [],
    // 公眾號更新時候呼叫的通知函式
    notify() {
        const len = this.followList.length;
        if (len > 0) {
            // 通知已關注該公眾號的每個使用者,有內容更新
            for (let user of this.followList) {
                user.update();
            }
        }
    },
    // 新增新的關注的使用者
    add(user) {
        this.followList.push(user);
    },
    // 移除不再關注的使用者
    remove(user) {
        const idx = this.followList.indexOf(user);
        if (idx !== -1) {
            this.followList.splice(idx, 1);
        }
    },
    // 計算關注公眾號的總的使用者數
    count() {
        return this.followList.length;
    },
};

// 新建使用者的類
class User {
    constructor(name) {
        this.name = name;
    }
    // 接收公眾號內容更新的通知
    update() {
        console.log(`${this.name}接收到了公眾號的內容更新`);
    }
}

// 建立兩個新的使用者
const zhangSan = new User('張三');
const liSi = new User('李四');

// 公眾號新增關注的使用者
officialAccount.add(zhangSan);
officialAccount.add(liSi);

// 公眾號有新內容更新
officialAccount.notify();
console.log(`當前關注公眾號的使用者數量是:${officialAccount.count()}`);

// 張三不再關注公眾號
officialAccount.remove(zhangSan);

// 公眾號有新內容更新
officialAccount.notify();
console.log(`當前關注公眾號的使用者數量是:${officialAccount.count()}`);

輸出的結果如下:

張三接收到了公眾號的內容更新
李四接收到了公眾號的內容更新
當前關注公眾號的使用者數量是:2
李四接收到了公眾號的內容更新
當前關注公眾號的使用者數量是:1

上面的程式碼完善了關注和取消關注的過程,並且可以獲取當前公眾號的關注人數。我們還實現了一個使用者類,能夠讓我們快速建立需要新增到公眾號關注列表的使用者。當然你也可以把公眾號的實現通過一個類來完成,這裡就不再展示實現的過程了。

通過上面這個簡單的例子,你是不是有所感悟,有了一些新的收穫?我們上面實現的其實就是一個簡單的觀察者模式。接下來我們來聊一聊觀察者模式的定義,以及一些在實際開發中的用途。

觀察者模式的定義

所謂的觀察者模式指的是一種一對多的關係,我們把其中的叫做Subject(類比上文中的公眾號),把其中的叫做Observer(類比上文中關注公眾號的使用者),也就是觀察者。因為多個Observer的變動依賴Subject的狀態更新,所以Subject在內部維護了一個Observer的列表,一旦Subject的狀態有更新,就會遍歷這個列表,通知列表中每一個Observer進行相應的更新。因為有了這個列表,Subject就可以對這個列表進行增刪改查的操作。也就實現了ObserverSubject依賴的更新和解綁

我們來看一下觀察者模式的UML圖:

觀察者模式UML模式

從上圖我們這可以看到,對於Subject來說,它自身需要維護一個observerCollection,這個列表裡面就是Observer的例項。然後在Subject內部實現了增加觀察者,移除觀察者,和通知觀察者的方法。其中通知觀察者的方式就是遍歷observerCollection列表,依次呼叫列表中每一個observerupdate方法。

到這裡為止,你現在已經對這個設計模式有了一些瞭解。那我們學習這個設計模式有什麼作用呢?首先如果我們在開發中遇到這種類似上面的一對多的關係,並且的狀態更新依賴的狀態;那麼我們就可以使用這種設計模式去解決這種問題。而且我們也可以使用這種模式解耦我們的程式碼,讓我們的程式碼更好擴充與維護

當然一些同學會覺得自己在平時的開發中好像沒怎麼使用過這種設計模式,那是因為我們平時在開發中一般都會使用一些框架,比如Vue或者React等,這個設計模式已經被這些框架在內部實現好了。我們可以直接使用,所以我們對這個設計模式的感知會少一些

實戰:實現一個簡單的TODO小應用

我們可以使用觀察者模式實現一個小應用,這個應用很簡單,就是能夠讓使用者新增自己的待辦,並且需要顯示已新增的待辦事項的數量

瞭解了需求之後,我們需要確定那些是,哪些是。當然我們知道整個TODO的狀態就是我們所說的,那麼對於待辦列表的展示以及待辦列表的計數就是我們所說的。理清了思路之後,實現這個小應用就變得很簡單了。

可以點選?這裡提前體驗一下這個簡單的小應用

TODO小應用

首先我們需要先實現觀察者模式中的SubjectObserver類,程式碼如下所示。

Subject類:

// Subject
class Subject {
    constructor() {
        this.observerCollection = [];
    }
    // 新增觀察者
    registerObserver(observer) {
        this.observerCollection.push(observer);
    }
    // 移除觀察者
    unregisterObserver(observer) {
        const observerIndex = this.observerCollection.indexOf(observer);
        this.observerCollection.splice(observerIndex, 1);
    }
    // 通知觀察者
    notifyObservers(subject) {
        const collection = this.observerCollection;
        const len = collection.length;
        if (len > 0) {
            for (let observer of collection) {
                observer.update(subject);
            }
        }
    }
}

Observer類:

// 觀察者
class Observer {
    update() {}
}

那麼接下來的程式碼就是關於上面待辦的具體實現了,程式碼中也新增了相應的註釋,我們來看一下。

待辦應用的邏輯部分:

// 表單的狀態
class Todo extends Subject {
    constructor() {
        super();
        this.items = [];
    }
    // 新增todo
    addItem(item) {
        this.items.push(item);
        super.notifyObservers(this);
    }
}

// 列表渲染
class ListRender extends Observer {
    constructor(el) {
        super();
        this.el = document.getElementById(el);
    }
    // 更新列表
    update(todo) {
        super.update();
        const items = todo.items;
        this.el.innerHTML = items.map(text => `<li>${text}</li>`).join('');
    }
}

// 列表計數觀察者
class CountObserver extends Observer {
    constructor(el) {
        super();
        this.el = document.getElementById(el);
    }
    // 更新計數
    update(todo) {
        this.el.innerText = `${todo.items.length}`;
    }
}

// 列表觀察者
const listObserver = new ListRender('item-list');
// 計數觀察者
const countObserver = new CountObserver('item-count');

const todo = new Todo();
// 新增列表觀察者
todo.registerObserver(listObserver);
// 新增計數觀察者
todo.registerObserver(countObserver);

// 獲取todo按鈕
const addBtn = document.getElementById('add-btn');
// 獲取輸入框的內容
const inputEle = document.getElementById('new-item');
addBtn.onclick = () => {
    const item = inputEle.value;
    // 判斷新增的內容是否為空
    if (item) {
        todo.addItem(item);
        inputEle.value = '';
    }
};

從上面的程式碼我們可以清楚地知道這個應用的每一個部分,被觀察的Subject就是我們的todo物件,它的狀態就是待辦列表。它維護的觀察者列表分別是展示待辦列表的listObserver和展示待辦數量的countObserver。一旦todo的列表新增加了一項待辦,那麼就會通知這兩個觀察者去做相應的內容更新。這樣程式碼的邏輯就很直觀明瞭。如果以後在狀態變更的時候還要新增新的功能,我們只需要再次新增一個相應的observer就可以了,維護起來也很方便。

當然上面的程式碼只實現了很基礎的功能,還沒有包含待辦的完成和刪除,以及對於未完成和已完成的待辦的分類展示。而且列表的渲染每次都是重新渲染的,沒有複用的邏輯。因為我們本章的內容是跟大家一起來探討一下觀察者模式,所以上面的程式碼比較簡陋,也只是為了說明觀察者模式的用法。相信優秀的你能夠在這個基礎上,把這些功能都完善好,快去試試吧。

其實我們學習這些設計模式,都是為了讓程式碼的邏輯更加清晰明瞭,能夠複用一些程式碼的邏輯,減少重複的工作,提升開發的效率。讓整個應用更加容易維護和擴充。當然不能為了使用而使用,在使用之前,需要對當前的問題做一個全面的瞭解。到底需不需要使用某個設計模式是一個需要考慮清楚的問題。

好啦,關於觀察者模式到這裡就結束啦,大家如果有什麼意見和建議可以在文章下面下面留言,我們一起探討一下。也可以在這裡提出來,我們更好地進行討論。也歡迎大家關注我的公眾號關山不難越,隨時隨地獲取文章的更新。

參考連結:

文章封面圖來源:unDraw

相關文章