嗨 大家好 我是一路爬坡的阿肥
晚上就要準備搭去澎湖啦!
接下來四天阿肥會提醒自己
醒來的第一件事不是衝去看海
是趕快PO文!
我們先定義這個服務中,會使用到的資料的值與型態。我們用可列舉的型別 enum
來宣告結果內容和城市的代號,來幫助我們在之後可以做相關列舉的操作。
另外,我們定義了 I_NotifyData
,作為 Observerable 在通知各個 Observer 的方法參數。
export enum E_Result { 'not_yet' = 'not_yet', 'yes' = 'yes', 'no' = 'no' }
export enum E_CityCode { 'tpe' = 'tpe', 'chung' = 'chung', 'nan' = 'nan', 'ka' = 'ka' }
export interface I_NotifyData {
cityCode: E_CityCode;
result: E_Result;
}
定義了 Observer 最重要的 update()
需接收 E_Result
的值來實作。並且記錄 Observer 本身的 id 與 城市代碼。
interface I_Observer {
cityCode: E_CityCode;
clientId: string;
update: (result: E_Result) => void;
}
定義了三個最主要對 Observer 操作的方法 - addObserver()
增加 Observer;removeObserver()
移除 Observer;notifyObservers()
通知所有 Observer;
interface I_Observerable {
addObserver: (observer: I_Observer) => void;
removeObserver: (observer: I_Observer) => void;
notifyObservers: (o: I_NotifyData) => void;
}
這個類別實作的是管理訂閱者,以及發布通知的功能。除了實現 I_Observerable
介面,還需自己紀錄訂閱者列表。這個列表我們需依城市來劃分,分別紀錄有訂閱的 Observer。
class TyphoonNotifyCenter implements I_Observerable {
// 依城市來劃分,紀錄訂閱者列表
protected observerList: { [key in E_CityCode]: I_Observer[] } = {
tpe: [],
chung: [],
nan: [],
ka: [],
};
private getTargetList(code: E_CityCode): I_Observer[] {
return this.observerList[code];
}
private findObserverIndex(targetList: I_Observer[], observerId: string) {
return targetList.findIndex(o => o.clientId === observerId);
}
// 透過新增與移除方法來管理訂閱者列表
public addObserver(o: I_Observer): void {
let targetList = this.getTargetList(o.cityCode);
if (this.findObserverIndex(targetList, o.clientId) < 0) {
targetList.push(o);
}
}
public removeObserver(o: I_Observer): void {
let targetList = this.getTargetList(o.cityCode);
let i = this.findObserverIndex(targetList, o.clientId)
if (i > -1) {
targetList.splice(i, 1);
}
}
// 透過 forEach 來呼叫每個訂閱者的 update()
public notifyObservers(o: I_NotifyData): void {
let targetList = this.getTargetList(o.cityCode);
if (targetList.length) {
targetList.forEach(obs => obs.update(o.result));
}
}
}
這個類別實現了 I_Observer
,並且實作接收通知後的變動。
class TyphoonNotifiedClient implements I_Observer {
public cityCode: E_CityCode;
public clientId: string;
public updateCallback: (result: E_Result) => void;
constructor(clientId: string, cityCode: E_CityCode) {
this.cityCode = cityCode;
this.clientId = clientId;
}
// 提供外部UI調用,設定update() 執行的 callback
public setUpdateCallback(callback): void {
this.updateCallback = callback;
}
public update(result: E_Result): void {
if (this.updateCallback) this.updateCallback(result);
}
}
由於真實情況不屬於程式的範疇。所以我們寫個小程式來模擬一下吧。這個小程式會每隔2秒隨機取某個縣市來決定是否放颱風假。決定之後,就會執行 TyphoonNotifyCenter
實體的 notifyObservers()
來通知對應的 TyphoonNotifiedClient
們。
const getRandomItem = arr => arr[Math.floor(Math.random() * arr.length)];
function startDesiding(center: Obs.TyphoonNotifyCenter) {
alert('開始決定各縣市是否放颱風假,請按確定繼續');
// 利用 Object.keys 對列舉型別取值
let cityCodeList: string[] = Object.keys(Obs.E_CityCode).map(c => (c));
let resultList: string[] = Object.keys(Obs.E_Result).map(c => (c));
resultList = resultList.filter(s => s !== 'not_yet');
// 主程式
let timer = setInterval(() => {
if (!cityCodeList.length) {
clearInterval(timer);
alert('各縣市已決定完是否放颱風假');
return;
}
let curCity = getRandomItem(cityCodeList);
let result = getRandomItem(resultList);
cityCodeList = cityCodeList.filter(c => c !== curCity);
if (center)
center.notifyObservers({ cityCode: curCity, result: result });
}, 2000);
}
這次的UI非常簡單,只要傳入 observer 的實體作為prop,顯示 observer 的相關資訊,以及用 useState()
管理最後結果的顯示狀態即可。
const CityCodeWordDict: { [key in Obs.E_CityCode]: string } = {
'tpe': '北北基',
'chung': '台中',
'nan': '台南',
'ka': '高雄'
}
const ResultWordDict: { [key in Obs.E_Result]: string } = {
'not_yet': '尚未公布',
'yes': '明日停班停課',
'no': '明天照常上班上課'
}
export interface I_Props_TyphoonNotifier {
notifier: Obs.TyphoonNotifiedClient;
}
export const TyphoonNotifier: React.FC<I_Props_TyphoonNotifier> = ({ notifier }) => {
const [result, setResult] = React.useState<Obs.E_Result>(Obs.E_Result.not_yet);
// 將setResult 作為 update() 的 callback
notifier.setUpdateCallback(setResult);
return <div>
<ul>
<li>名稱:{notifier.clientId}</li>
<li>關注的城市:{CityCodeWordDict[notifier.cityCode]}</li>
<li className={result}>是否放颱風假:{ResultWordDict[result]}</li>
</ul>
</div>
}
另外我們做個通知中心的元件,當元件渲染完成,就呼叫 startDesiding()
開始執行模擬程式。
export interface I_Props_TyphoonCenter {
center: Obs.TyphoonNotifyCenter;
}
export const TyphoonCenter: React.FC<I_Props_TyphoonCenter> = ({ center, children }) => {
// 傳入空陣列,相當於 React ComponentDidMount
React.useEffect(() => {
startDesiding(center)
}, []);
return <div>{children}</div>
}
let notifierCenter = new Obs.TyphoonNotifyCenter();
let notifier1 = new Obs.TyphoonNotifiedClient("台北李先生", Obs.E_CityCode.tpe);
notifierCenter.addObserver(notifier1);
let notifier2 = new Obs.TyphoonNotifiedClient("台中莊同學", Obs.E_CityCode.chung);
notifierCenter.addObserver(notifier2);
let notifier3 = new Obs.TyphoonNotifiedClient("台南蔡小姐", Obs.E_CityCode.nan);
notifierCenter.addObserver(notifier3);
let notifier4 = new Obs.TyphoonNotifiedClient("高雄陳先生", Obs.E_CityCode.ka);
notifierCenter.addObserver(notifier4);
<TyphoonCenter center={notifierCenter}>
<TyphoonNotifier notifier={notifier1} />
<TyphoonNotifier notifier={notifier2} />
<TyphoonNotifier notifier={notifier3} />
<TyphoonNotifier notifier={notifier4} />
</TyphoonCenter>
執行yarn story
後,開啟http://localhost:6006
,然後切到Observer Pattern/TyphoonNotifier
,就可以看到畫面了。
觀察者模式算是在設計模式裡面蠻淺顯易懂的,而且在寫需要廣播或特定的訂閱發送功能時,這就相當適合。不過在React中,如果有將資料放在context或是reducer的話,就不需再套入這個模式了,因為他們的機制也是像觀察者模式一樣,有任何變動就會通知有使用到資料的元件進行更新。
那這個模式是不是在React中就無用武之地?也不是,有些資料如果是大量且持續變動的,例如看盤軟體中上千檔的股價跳動,你自然不會想記在context或是reducer拖垮渲染效能。這時候你就能套入觀察者模式加以擴充,就能派上用場啦。
今天的程式實作會在 github 的 packages/src/day25-behavioral.observer.code