1. 設(shè)計(jì)模式大冒險(xiǎn)第一關(guān):觀察者模式

        共 5975字,需瀏覽 12分鐘

         ·

        2020-10-23 16:40

        最近把之前學(xué)習(xí)過的這些設(shè)計(jì)模式又再次溫習(xí)了一下,覺得還是有很多收獲的。確實(shí)有了溫故知新的感覺,所以準(zhǔn)備在每個(gè)設(shè)計(jì)模式復(fù)習(xí)完之后都能夠?qū)懸黄P(guān)于這個(gè)設(shè)計(jì)模式的文章,這樣會(huì)讓自己能夠加深對(duì)這個(gè)設(shè)計(jì)模式的理解;也能夠跟大家一起來探討一下。

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

        生活中的小例子

        相信大家都關(guān)注過一些公眾號(hào),那么對(duì)于一個(gè)公眾號(hào)來說,如果有新的文章發(fā)布的話;那么所有關(guān)注這個(gè)公眾號(hào)的用戶都會(huì)收到更新的通知,如果一個(gè)用戶沒有關(guān)注或者關(guān)注后又取消了關(guān)注,那么這個(gè)用戶就不會(huì)收到該公眾號(hào)更新的通知。相信這個(gè)場景大家都很熟悉吧。那么如果我們把這個(gè)過程抽象出來,用代碼來實(shí)現(xiàn)的話,你會(huì)怎么處理呢?不妨現(xiàn)在停下來思考一下。

        通過上面的描述,我們知道這是一個(gè)一對(duì)多的關(guān)系。也就是一個(gè)公眾號(hào)對(duì)應(yīng)著許多關(guān)注這個(gè)公眾號(hào)的用戶。

        關(guān)注公眾號(hào)

        那么對(duì)于這個(gè)公眾號(hào)來說,它的內(nèi)部需要有一個(gè)列表記錄著關(guān)注這個(gè)公眾號(hào)的用戶,一旦公眾號(hào)有了新的內(nèi)容。那么對(duì)于公眾號(hào)來說,它會(huì)遍歷這個(gè)列表。然后給列表中的每一個(gè)用戶發(fā)送一個(gè)內(nèi)容跟新的通知。我們可以通過代碼來表示這個(gè)過程:

        //?用戶
        const?user?=?{
        ?update()?{
        ??console.log('公眾號(hào)更新了新的內(nèi)容');
        ?},
        };

        //?公眾號(hào)
        const?officialAccount?=?{
        ????//?關(guān)注當(dāng)前公眾號(hào)的用戶列表
        ?followList:?[user],
        ????//?公眾號(hào)更新時(shí)候調(diào)用的通知函數(shù)
        ?notify()?{
        ??const?len?=?this.followList.length;
        ??if?(len?>?0)?{
        ??????//?通知已關(guān)注該公眾號(hào)的每個(gè)用戶,有內(nèi)容更新
        ???for?(let?user?of?this.followList)?{
        ????user.update();
        ???}
        ??}
        ?},
        };

        //?公眾號(hào)有新內(nèi)容更新
        officialAccount.notify();

        運(yùn)行的結(jié)果如下:

        公眾號(hào)更新了新的內(nèi)容

        上面的代碼能夠簡單的表示,當(dāng)公眾號(hào)的內(nèi)容發(fā)生了更新的時(shí)候,去通知關(guān)注該公眾號(hào)的用戶的過程。但是這個(gè)實(shí)現(xiàn)是很簡陋的,還缺少一些內(nèi)容。我們接下來把這些缺少的過程補(bǔ)充完整。對(duì)于公眾號(hào)來說,還需要可以添加新的關(guān)注的用戶,移除不再關(guān)注的用戶,獲取關(guān)注公眾號(hào)的用戶總數(shù)等。我們來實(shí)現(xiàn)一下上面的過程:

        //?公眾號(hào)
        const?officialAccount?=?{
        ?//?關(guān)注當(dāng)前公眾號(hào)的用戶列表
        ?followList:?[],
        ?//?公眾號(hào)更新時(shí)候調(diào)用的通知函數(shù)
        ?notify()?{
        ??const?len?=?this.followList.length;
        ??if?(len?>?0)?{
        ???//?通知已關(guān)注該公眾號(hào)的每個(gè)用戶,有內(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);
        ??}
        ?},
        ?//?計(jì)算關(guān)注公眾號(hào)的總的用戶數(shù)
        ?count()?{
        ??return?this.followList.length;
        ?},
        };

        //?新建用戶的類
        class?User?{
        ?constructor(name)?{
        ??this.name?=?name;
        ?}
        ?//?接收公眾號(hào)內(nèi)容更新的通知
        ?update()?{
        ??console.log(`${this.name}接收到了公眾號(hào)的內(nèi)容更新`);
        ?}
        }

        //?創(chuàng)建兩個(gè)新的用戶
        const?zhangSan?=?new?User('張三');
        const?liSi?=?new?User('李四');

        //?公眾號(hào)添加關(guān)注的用戶
        officialAccount.add(zhangSan);
        officialAccount.add(liSi);

        //?公眾號(hào)有新內(nèi)容更新
        officialAccount.notify();
        console.log(`當(dāng)前關(guān)注公眾號(hào)的用戶數(shù)量是:${officialAccount.count()}`);

        //?張三不再關(guān)注公眾號(hào)
        officialAccount.remove(zhangSan);

        //?公眾號(hào)有新內(nèi)容更新
        officialAccount.notify();
        console.log(`當(dāng)前關(guān)注公眾號(hào)的用戶數(shù)量是:${officialAccount.count()}`);

        輸出的結(jié)果如下:

        張三接收到了公眾號(hào)的內(nèi)容更新
        李四接收到了公眾號(hào)的內(nèi)容更新
        當(dāng)前關(guān)注公眾號(hào)的用戶數(shù)量是:2
        李四接收到了公眾號(hào)的內(nèi)容更新
        當(dāng)前關(guān)注公眾號(hào)的用戶數(shù)量是:1

        上面的代碼完善了關(guān)注和取消關(guān)注的過程,并且可以獲取當(dāng)前公眾號(hào)的關(guān)注人數(shù)。我們還實(shí)現(xiàn)了一個(gè)用戶類,能夠讓我們快速創(chuàng)建需要添加到公眾號(hào)關(guān)注列表的用戶。當(dāng)然你也可以把公眾號(hào)的實(shí)現(xiàn)通過一個(gè)類來完成,這里就不再展示實(shí)現(xiàn)的過程了。

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

        觀察者模式的定義

        所謂的觀察者模式指的是一種一對(duì)多的關(guān)系,我們把其中的叫做Subject(類比上文中的公眾號(hào)),把其中的叫做Observer(類比上文中關(guān)注公眾號(hào)的用戶),也就是觀察者。因?yàn)槎鄠€(gè)Observer的變動(dòng)依賴Subject的狀態(tài)更新,所以Subject在內(nèi)部維護(hù)了一個(gè)Observer的列表,一旦Subject的狀態(tài)有更新,就會(huì)遍歷這個(gè)列表,通知列表中每一個(gè)Observer進(jìn)行相應(yīng)的更新。因?yàn)橛辛诉@個(gè)列表,Subject就可以對(duì)這個(gè)列表進(jìn)行增刪改查的操作。也就實(shí)現(xiàn)了Observer對(duì)Subject依賴的更新和解綁。

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

        觀察者模式UML模式

        從上圖我們這可以看到,對(duì)于Subject來說,它自身需要維護(hù)一個(gè)observerCollection,這個(gè)列表里面就是Observer的實(shí)例。然后在Subject內(nèi)部實(shí)現(xiàn)了增加觀察者,移除觀察者,和通知觀察者的方法。其中通知觀察者的方式就是遍歷observerCollection列表,依次調(diào)用列表中每一個(gè)observerupdate方法。

        到這里為止,你現(xiàn)在已經(jīng)對(duì)這個(gè)設(shè)計(jì)模式有了一些了解。那我們學(xué)習(xí)這個(gè)設(shè)計(jì)模式有什么作用呢?首先如果我們在開發(fā)中遇到這種類似上面的一對(duì)多的關(guān)系,并且的狀態(tài)更新依賴的狀態(tài);那么我們就可以使用這種設(shè)計(jì)模式去解決這種問題。而且我們也可以使用這種模式解耦我們的代碼,讓我們的代碼更好拓展與維護(hù)。

        當(dāng)然一些同學(xué)會(huì)覺得自己在平時(shí)的開發(fā)中好像沒怎么使用過這種設(shè)計(jì)模式,那是因?yàn)槲覀兤綍r(shí)在開發(fā)中一般都會(huì)使用一些框架,比如Vue或者React等,這個(gè)設(shè)計(jì)模式已經(jīng)被這些框架在內(nèi)部實(shí)現(xiàn)好了。我們可以直接使用,所以我們對(duì)這個(gè)設(shè)計(jì)模式的感知會(huì)少一些

        實(shí)戰(zhàn):實(shí)現(xiàn)一個(gè)簡單的TODO小應(yīng)用

        之前我開發(fā)的一個(gè)組隊(duì)打卡小程序主線程,其中首頁待辦的一些功能就使用了觀察者模式,今天我們利用觀察者模式實(shí)現(xiàn)一個(gè)粗糙的版本就是能夠讓用戶添加自己的待辦,并且需要顯示已添加的待辦事項(xiàng)的數(shù)量。

        了解了需求之后,我們需要確定那些是,哪些是。當(dāng)然我們知道整個(gè)TODO的狀態(tài)就是我們所說的,那么對(duì)于待辦列表的展示以及待辦列表的計(jì)數(shù)就是我們所說的。理清了思路之后,實(shí)現(xiàn)這個(gè)小應(yīng)用就變得很簡單了。

        可以點(diǎn)擊?這里提前體驗(yàn)一下這個(gè)簡單的小應(yīng)用。

        TODO小應(yīng)用

        首先我們需要先實(shí)現(xiàn)觀察者模式中的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()?{}
        }

        那么接下來的代碼就是關(guān)于上面待辦的具體實(shí)現(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?=>?`
      2. ${text}
      3. `
        ).join('');
        ?}
        }

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

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

        const?todo?=?new?Todo();
        //?添加列表觀察者
        todo.registerObserver(listObserver);
        //?添加計(jì)數(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?=?'';
        ?}
        };

        從上面的代碼我們可以清楚地知道這個(gè)應(yīng)用的每一個(gè)部分,被觀察的Subject就是我們的todo對(duì)象,它的狀態(tài)就是待辦列表。它維護(hù)的觀察者列表分別是展示待辦列表的listObserver和展示待辦數(shù)量的countObserver。一旦todo的列表新增加了一項(xiàng)待辦,那么就會(huì)通知這兩個(gè)觀察者去做相應(yīng)的內(nèi)容更新。這樣代碼的邏輯就很直觀明了。如果以后在狀態(tài)變更的時(shí)候還要添加新的功能,我們只需要再次添加一個(gè)相應(yīng)的observer就可以了,維護(hù)起來也很方便。

        當(dāng)然上面的代碼只實(shí)現(xiàn)了很基礎(chǔ)的功能,還沒有包含待辦的完成和刪除,以及對(duì)于未完成和已完成的待辦的分類展示。而且列表的渲染每次都是重新渲染的,沒有復(fù)用的邏輯。因?yàn)槲覀儽菊碌膬?nèi)容是跟大家一起來探討一下觀察者模式,所以上面的代碼比較簡陋,也只是為了說明觀察者模式的用法。相信優(yōu)秀的你能夠在這個(gè)基礎(chǔ)上,把這些功能都完善好,快去試試吧。

        其實(shí)我們學(xué)習(xí)這些設(shè)計(jì)模式,都是為了讓代碼的邏輯更加清晰明了,能夠復(fù)用一些代碼的邏輯,減少重復(fù)的工作,提升開發(fā)的效率。讓整個(gè)應(yīng)用更加容易維護(hù)和拓展。當(dāng)然不能為了使用而使用,在使用之前,需要對(duì)當(dāng)前的問題做一個(gè)全面的了解。到底需不需要使用某個(gè)設(shè)計(jì)模式是一個(gè)需要考慮清楚的問題。

        好啦,關(guān)于觀察者模式到這里就結(jié)束啦,大家如果有什么意見和建議可以在文章下面下面留言,我們一起探討一下。也可以在這里提出來,我們更好地進(jìn)行討論。也歡迎大家關(guān)注我的公眾號(hào)關(guān)山不難越,隨時(shí)隨地獲取文章的更新。

        大家如果有興趣的話,可以看一下我之前的作品主線程,如果這個(gè)小程序能夠?qū)δ阌幸恍椭脑捑透昧藒

        參考鏈接:

        • The Observer Pattern
        • How to Use the Observable Pattern in JavaScript

        文章封面圖來源:unDraw


        關(guān)注我

        大家也可以關(guān)注我的公眾號(hào)《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認(rèn)識(shí)你不知道的前端。



        瀏覽 52
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 亚洲欧美日韩在线不卡 | 女郎av在线 | 翔田千里一区二在线观看 | 娜美被扒开腿做同人软件 | 亚洲国产精品一区二区三区 |