設(shè)計模式大冒險第一關(guān):觀察者模式
最近把之前學(xué)習(xí)過的這些設(shè)計模式又再次溫習(xí)了一下,覺得還是有很多收獲的。確實有了溫故知新的感覺,所以準(zhǔn)備在每個設(shè)計模式復(fù)習(xí)完之后都能夠?qū)懸黄P(guān)于這個設(shè)計模式的文章,這樣會讓自己能夠加深對這個設(shè)計模式的理解;也能夠跟大家一起來探討一下。
今天我們來一起學(xué)習(xí)一下觀察者模式,剛開始我們不需要知道觀察者模式的定義是什么,這些我們到后面再去了解。我想先帶著大家從生活中的一個小事例開始。從生活中熟悉的事情入手,會讓我們更快速的理解這個模式的用途。
生活中的小例子
相信大家都關(guān)注過一些公眾號,那么對于一個公眾號來說,如果有新的文章發(fā)布的話;那么所有關(guān)注這個公眾號的用戶都會收到更新的通知,如果一個用戶沒有關(guān)注或者關(guān)注后又取消了關(guān)注,那么這個用戶就不會收到該公眾號更新的通知。相信這個場景大家都很熟悉吧。那么如果我們把這個過程抽象出來,用代碼來實現(xiàn)的話,你會怎么處理呢?不妨現(xiàn)在停下來思考一下。
通過上面的描述,我們知道這是一個一對多的關(guān)系。也就是一個公眾號對應(yīng)著許多關(guān)注這個公眾號的用戶。

那么對于這個公眾號來說,它的內(nèi)部需要有一個列表記錄著關(guān)注這個公眾號的用戶,一旦公眾號有了新的內(nèi)容。那么對于公眾號來說,它會遍歷這個列表。然后給列表中的每一個用戶發(fā)送一個內(nèi)容跟新的通知。我們可以通過代碼來表示這個過程:
//?用戶
const?user?=?{
?update()?{
??console.log('公眾號更新了新的內(nèi)容');
?},
};
//?公眾號
const?officialAccount?=?{
????//?關(guān)注當(dāng)前公眾號的用戶列表
?followList:?[user],
????//?公眾號更新時候調(diào)用的通知函數(shù)
?notify()?{
??const?len?=?this.followList.length;
??if?(len?>?0)?{
??????//?通知已關(guān)注該公眾號的每個用戶,有內(nèi)容更新
???for?(let?user?of?this.followList)?{
????user.update();
???}
??}
?},
};
//?公眾號有新內(nèi)容更新
officialAccount.notify();
運行的結(jié)果如下:
公眾號更新了新的內(nèi)容
上面的代碼能夠簡單的表示,當(dāng)公眾號的內(nèi)容發(fā)生了更新的時候,去通知關(guān)注該公眾號的用戶的過程。但是這個實現(xiàn)是很簡陋的,還缺少一些內(nèi)容。我們接下來把這些缺少的過程補充完整。對于公眾號來說,還需要可以添加新的關(guān)注的用戶,移除不再關(guān)注的用戶,獲取關(guān)注公眾號的用戶總數(shù)等。我們來實現(xiàn)一下上面的過程:
//?公眾號
const?officialAccount?=?{
?//?關(guān)注當(dāng)前公眾號的用戶列表
?followList:?[],
?//?公眾號更新時候調(diào)用的通知函數(shù)
?notify()?{
??const?len?=?this.followList.length;
??if?(len?>?0)?{
???//?通知已關(guān)注該公眾號的每個用戶,有內(nèi)容更新
???for?(let?user?of?this.followList)?{
????user.update();
???}
??}
?},
?//?添加新的關(guān)注的用戶
?add(user)?{
??this.followList.push(user);
?},
?//?移除不再關(guān)注的用戶
?remove(user)?{
??const?idx?=?this.followList.indexOf(user);
??if?(idx?!==?-1)?{
???this.followList.splice(idx,?1);
??}
?},
?//?計算關(guān)注公眾號的總的用戶數(shù)
?count()?{
??return?this.followList.length;
?},
};
//?新建用戶的類
class?User?{
?constructor(name)?{
??this.name?=?name;
?}
?//?接收公眾號內(nèi)容更新的通知
?update()?{
??console.log(`${this.name}接收到了公眾號的內(nèi)容更新`);
?}
}
//?創(chuàng)建兩個新的用戶
const?zhangSan?=?new?User('張三');
const?liSi?=?new?User('李四');
//?公眾號添加關(guān)注的用戶
officialAccount.add(zhangSan);
officialAccount.add(liSi);
//?公眾號有新內(nèi)容更新
officialAccount.notify();
console.log(`當(dāng)前關(guān)注公眾號的用戶數(shù)量是:${officialAccount.count()}`);
//?張三不再關(guān)注公眾號
officialAccount.remove(zhangSan);
//?公眾號有新內(nèi)容更新
officialAccount.notify();
console.log(`當(dāng)前關(guān)注公眾號的用戶數(shù)量是:${officialAccount.count()}`);
輸出的結(jié)果如下:
張三接收到了公眾號的內(nèi)容更新
李四接收到了公眾號的內(nèi)容更新
當(dāng)前關(guān)注公眾號的用戶數(shù)量是:2
李四接收到了公眾號的內(nèi)容更新
當(dāng)前關(guān)注公眾號的用戶數(shù)量是:1
上面的代碼完善了關(guān)注和取消關(guān)注的過程,并且可以獲取當(dāng)前公眾號的關(guān)注人數(shù)。我們還實現(xiàn)了一個用戶類,能夠讓我們快速創(chuàng)建需要添加到公眾號關(guān)注列表的用戶。當(dāng)然你也可以把公眾號的實現(xiàn)通過一個類來完成,這里就不再展示實現(xiàn)的過程了。
通過上面這個簡單的例子,你是不是有所感悟,有了一些新的收獲?我們上面實現(xiàn)的其實就是一個簡單的觀察者模式。接下來我們來聊一聊觀察者模式的定義,以及一些在實際開發(fā)中的用途。
觀察者模式的定義
所謂的觀察者模式指的是一種一對多的關(guān)系,我們把其中的一叫做Subject(類比上文中的公眾號),把其中的多叫做Observer(類比上文中關(guān)注公眾號的用戶),也就是觀察者。因為多個Observer的變動依賴Subject的狀態(tài)更新,所以Subject在內(nèi)部維護了一個Observer的列表,一旦Subject的狀態(tài)有更新,就會遍歷這個列表,通知列表中每一個Observer進行相應(yīng)的更新。因為有了這個列表,Subject就可以對這個列表進行增刪改查的操作。也就實現(xiàn)了Observer對Subject依賴的更新和解綁。
我們來看一下觀察者模式的UML圖:

從上圖我們這可以看到,對于Subject來說,它自身需要維護一個observerCollection,這個列表里面就是Observer的實例。然后在Subject內(nèi)部實現(xiàn)了增加觀察者,移除觀察者,和通知觀察者的方法。其中通知觀察者的方式就是遍歷observerCollection列表,依次調(diào)用列表中每一個observer的update方法。
到這里為止,你現(xiàn)在已經(jīng)對這個設(shè)計模式有了一些了解。那我們學(xué)習(xí)這個設(shè)計模式有什么作用呢?首先如果我們在開發(fā)中遇到這種類似上面的一對多的關(guān)系,并且多的狀態(tài)更新依賴一的狀態(tài);那么我們就可以使用這種設(shè)計模式去解決這種問題。而且我們也可以使用這種模式解耦我們的代碼,讓我們的代碼更好拓展與維護。
當(dāng)然一些同學(xué)會覺得自己在平時的開發(fā)中好像沒怎么使用過這種設(shè)計模式,那是因為我們平時在開發(fā)中一般都會使用一些框架,比如Vue或者React等,這個設(shè)計模式已經(jīng)被這些框架在內(nèi)部實現(xiàn)好了。我們可以直接使用,所以我們對這個設(shè)計模式的感知會少一些。
實戰(zhàn):實現(xiàn)一個簡單的TODO小應(yīng)用
之前我開發(fā)的一個組隊打卡小程序主線程,其中首頁待辦的一些功能就使用了觀察者模式,今天我們利用觀察者模式實現(xiàn)一個粗糙的版本,就是能夠讓用戶添加自己的待辦,并且需要顯示已添加的待辦事項的數(shù)量。
了解了需求之后,我們需要確定那些是一,哪些是多。當(dāng)然我們知道整個TODO的狀態(tài)就是我們所說的一,那么對于待辦列表的展示以及待辦列表的計數(shù)就是我們所說的多。理清了思路之后,實現(xiàn)這個小應(yīng)用就變得很簡單了。
可以點擊?這里提前體驗一下這個簡單的小應(yīng)用。

首先我們需要先實現(xiàn)觀察者模式中的Subject和Observer類,代碼如下所示。
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()?{}
}
那么接下來的代碼就是關(guān)于上面待辦的具體實現(xiàn)了,代碼中也添加了相應(yīng)的注釋,我們來看一下。
待辦應(yīng)用的邏輯部分:
//?表單的狀態(tài)
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?=>?`${text} `).join('');
?}
}
//?列表計數(shù)觀察者
class?CountObserver?extends?Observer?{
?constructor(el)?{
??super();
??this.el?=?document.getElementById(el);
?}
?//?更新計數(shù)
?update(todo)?{
??this.el.innerText?=?`${todo.items.length}`;
?}
}
//?列表觀察者
const?listObserver?=?new?ListRender('item-list');
//?計數(shù)觀察者
const?countObserver?=?new?CountObserver('item-count');
const?todo?=?new?Todo();
//?添加列表觀察者
todo.registerObserver(listObserver);
//?添加計數(shù)觀察者
todo.registerObserver(countObserver);
//?獲取todo按鈕
const?addBtn?=?document.getElementById('add-btn');
//?獲取輸入框的內(nèi)容
const?inputEle?=?document.getElementById('new-item');
addBtn.onclick?=?()?=>?{
?const?item?=?inputEle.value;
?//?判斷添加的內(nèi)容是否為空
?if?(item)?{
??todo.addItem(item);
??inputEle.value?=?'';
?}
};
從上面的代碼我們可以清楚地知道這個應(yīng)用的每一個部分,被觀察的Subject就是我們的todo對象,它的狀態(tài)就是待辦列表。它維護的觀察者列表分別是展示待辦列表的listObserver和展示待辦數(shù)量的countObserver。一旦todo的列表新增加了一項待辦,那么就會通知這兩個觀察者去做相應(yīng)的內(nèi)容更新。這樣代碼的邏輯就很直觀明了。如果以后在狀態(tài)變更的時候還要添加新的功能,我們只需要再次添加一個相應(yīng)的observer就可以了,維護起來也很方便。
當(dāng)然上面的代碼只實現(xiàn)了很基礎(chǔ)的功能,還沒有包含待辦的完成和刪除,以及對于未完成和已完成的待辦的分類展示。而且列表的渲染每次都是重新渲染的,沒有復(fù)用的邏輯。因為我們本章的內(nèi)容是跟大家一起來探討一下觀察者模式,所以上面的代碼比較簡陋,也只是為了說明觀察者模式的用法。相信優(yōu)秀的你能夠在這個基礎(chǔ)上,把這些功能都完善好,快去試試吧。
其實我們學(xué)習(xí)這些設(shè)計模式,都是為了讓代碼的邏輯更加清晰明了,能夠復(fù)用一些代碼的邏輯,減少重復(fù)的工作,提升開發(fā)的效率。讓整個應(yīng)用更加容易維護和拓展。當(dāng)然不能為了使用而使用,在使用之前,需要對當(dāng)前的問題做一個全面的了解。到底需不需要使用某個設(shè)計模式是一個需要考慮清楚的問題。
好啦,關(guān)于觀察者模式到這里就結(jié)束啦,大家如果有什么意見和建議可以在文章下面下面留言,我們一起探討一下。也可以在這里提出來,我們更好地進行討論。也歡迎大家關(guān)注我的公眾號關(guān)山不難越,隨時隨地獲取文章的更新。
大家如果有興趣的話,可以看一下我之前的作品主線程,如果這個小程序能夠?qū)δ阌幸恍椭脑捑透昧藒
參考鏈接:
The Observer Pattern How to Use the Observable Pattern in JavaScript
文章封面圖來源:unDraw
關(guān)注我
大家也可以關(guān)注我的公眾號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。
