對(duì)不起,來(lái)晚了,御姐趣講設(shè)計(jì)模式
御姐力作,深入淺出,妙趣橫生,值得一看!
## 引言
你好,歡迎來(lái)到設(shè)計(jì)模式的世界,這一篇我將用一種引導(dǎo)、啟迪的思路去講述設(shè)計(jì)模式。在程序員的世界里,設(shè)計(jì)模式就相當(dāng)于武俠世界的劍招、套路。掌握了招式,你的武學(xué)修為會(huì)得到極大提升,最終達(dá)到無(wú)招勝有招的境界。
+ 首先,我會(huì)告訴大家設(shè)計(jì)模式是什么,不是什么。
+ 然后,簡(jiǎn)單介紹一下設(shè)計(jì)模式的分類,簡(jiǎn)單羅列一下各設(shè)計(jì)模式。
+ 接著,闡述面向?qū)ο笤O(shè)計(jì)一個(gè)非常重要的設(shè)計(jì)原則:**合成復(fù)用原則**,它是核心原則,提高復(fù)用一直是軟件工程師的不懈追求,它貫穿于設(shè)計(jì)模式一書。
+ 最后,從實(shí)用出發(fā),我會(huì)詳細(xì)描述兩個(gè)最經(jīng)典最常用的設(shè)計(jì)模式:?jiǎn)卫陀^察者。我不只是介紹這兩種模式的用途和實(shí)現(xiàn)方式,還會(huì)結(jié)合自己工作實(shí)踐,拋出限制與約束,提醒注意點(diǎn),以及跟其他模式的配合方式。
希望你學(xué)完這一節(jié),可以觸類旁通,在實(shí)際項(xiàng)目中用好設(shè)計(jì)模式,為社會(huì)做貢獻(xiàn)。
## 什么是設(shè)計(jì)模式
一門工程一定會(huì)有很多實(shí)踐性的經(jīng)驗(yàn)總結(jié)。就好比造大橋,人們會(huì)總結(jié)拱橋有哪些部件組成,有什么特點(diǎn),有什么適用場(chǎng)合,懸索橋又有什么部件、特點(diǎn)、使用場(chǎng)合。這些從實(shí)踐中提煉出來(lái)的建筑模式又可以指導(dǎo)新出現(xiàn)的需求,比如去設(shè)計(jì)一個(gè)某市長(zhǎng)江大橋,你會(huì)思考有哪個(gè)成熟的模式可以適用,在這個(gè)模式下,又要如何根據(jù)實(shí)際需求定制化地設(shè)計(jì)各個(gè)部件。
軟件工程也是如此。
設(shè)計(jì)模式是設(shè)計(jì)模式是軟件開發(fā)人員在軟件開發(fā)過(guò)程中面臨的一般問(wèn)題的解決方案,是被反復(fù)使用,多數(shù)人知曉的,經(jīng)過(guò)分類編目的代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。
+ 設(shè)計(jì)模式是一般問(wèn)題的解決方案。分析多種多樣的具體需求,常常會(huì)發(fā)現(xiàn)結(jié)構(gòu)上和行為上具有的共性,常常會(huì)產(chǎn)生相似的設(shè)計(jì)。設(shè)計(jì)模式是脫離了具體需求的,某類共性問(wèn)題的解決方案。
+ 設(shè)計(jì)模式是程序設(shè)計(jì)的經(jīng)驗(yàn)總結(jié)。在其適用范圍內(nèi)正確地使用設(shè)計(jì)模式通常會(huì)產(chǎn)生高質(zhì)量的設(shè)計(jì)。
+ 設(shè)計(jì)模式彌補(bǔ)了編程語(yǔ)言的缺陷。設(shè)計(jì)模式實(shí)現(xiàn)了創(chuàng)建時(shí)多態(tài)、雙重分派等在主流編程語(yǔ)言中不直接提供的功能。反過(guò)來(lái),近年來(lái)設(shè)計(jì)思想和設(shè)計(jì)模式的發(fā)展也影響了新興語(yǔ)言的語(yǔ)言規(guī)范。
+ 設(shè)計(jì)模式是軟件工程師的一套術(shù)語(yǔ)。完整地描述一個(gè)設(shè)計(jì)通常要花費(fèi)相當(dāng)?shù)钠?,通過(guò)對(duì)設(shè)計(jì)歸類,可以便于快速表達(dá)設(shè)計(jì)的特點(diǎn)。
## 設(shè)計(jì)模式不是什么
+ 不是普適原則。設(shè)計(jì)模式并不是如SOLID設(shè)計(jì)原則一樣是放之四海而皆準(zhǔn)的普適的原則。每個(gè)設(shè)計(jì)模式都有其適用場(chǎng)景,必須根據(jù)實(shí)際情況分析決定采用哪種設(shè)計(jì)模式或不使用設(shè)計(jì)模式。在一個(gè)軟件項(xiàng)目中設(shè)計(jì)模式并不是用得越多越好,符合實(shí)際需求的高質(zhì)量的獨(dú)特設(shè)計(jì)也是好設(shè)計(jì)。
+ 不是嚴(yán)格規(guī)范。設(shè)計(jì)模式是經(jīng)驗(yàn)的總結(jié),允許根據(jù)實(shí)際需要改變和改進(jìn)。采用了設(shè)計(jì)模式并不意味著類的結(jié)構(gòu)甚至命名都要與模式嚴(yán)格符合。在應(yīng)用設(shè)計(jì)模式時(shí)應(yīng)著重吸取其設(shè)計(jì)思路,根據(jù)實(shí)際需求進(jìn)行設(shè)計(jì)。尤其是很多設(shè)計(jì)模式中的名稱過(guò)于寬泛,在實(shí)際項(xiàng)目中并不適合用作類名。
+ 不是具體類庫(kù)。設(shè)計(jì)模式有助于代碼復(fù)用,但模式本身并不是可直接復(fù)用的代碼。在設(shè)計(jì)模式中擔(dān)任特定角色的并不是特定的一個(gè)類,通常需要在具體設(shè)計(jì)中結(jié)合具體需求來(lái)實(shí)現(xiàn)?,F(xiàn)代編程語(yǔ)言中的模板、泛型等語(yǔ)言特性有助于寫出更加通用的代碼,但對(duì)于很多設(shè)計(jì)模式,完全通用的代碼庫(kù)既難實(shí)現(xiàn),又難使用。
+ 不是行業(yè)解決方案。并沒(méi)有說(shuō)哪個(gè)模式特別適合互聯(lián)網(wǎng)、哪個(gè)模式專門針對(duì)自動(dòng)化。設(shè)計(jì)模式關(guān)注軟件結(jié)構(gòu)內(nèi)在的共性,而與具體的業(yè)務(wù)領(lǐng)域無(wú)關(guān)。
有工程師言必稱設(shè)計(jì)模式,生搬硬套設(shè)計(jì)模式,之后又出現(xiàn)反設(shè)計(jì)模式的思潮,認(rèn)為設(shè)計(jì)模式是騙局,無(wú)助于軟件質(zhì)量提升。我認(rèn)為,無(wú)論是神化設(shè)計(jì)模式亦或是反設(shè)計(jì)模式都是走極端,都是錯(cuò)誤的。設(shè)計(jì)模式為我們解決一些通用性的問(wèn)題提供了良好借鑒,且在大多數(shù)情況下,行之有效。設(shè)計(jì)模式并不絕對(duì)通用,在實(shí)際項(xiàng)目中如何抉擇用哪個(gè)設(shè)計(jì)模式或是不用設(shè)計(jì)模式,非??简?yàn)工程師的水平和經(jīng)驗(yàn)。
## GOF設(shè)計(jì)模式
設(shè)計(jì)模式的流行源于一本叫《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》的書,這本書的作者是4個(gè)博士,也叫GOF(Gang of Four),軟件設(shè)計(jì)模式一詞由作者從建筑設(shè)計(jì)領(lǐng)域引入計(jì)算機(jī)科學(xué)。
書中介紹了 23 種設(shè)計(jì)模式。這些模式可以分為三大類:
- + 創(chuàng)建型模式:?jiǎn)卫?、原型、工廠方法、抽象工廠、建造者
- + 結(jié)構(gòu)型模式:代理、適配、橋接、裝飾、外觀、享元、組合
- + 行為型模式:模板方法、策略、命令、職責(zé)鏈、狀態(tài)、觀察者、中介者、迭代器、訪問(wèn)者、備忘錄、解釋器
## 合成復(fù)用原則
對(duì)于軟件復(fù)用來(lái)說(shuō),組合優(yōu)于繼承,在軟件復(fù)用時(shí),優(yōu)先考慮組合關(guān)系,其次才考慮繼承關(guān)系。
面向?qū)ο笤O(shè)計(jì)的特點(diǎn)之一是繼承,子類包含父類的所有屬性和方法,因此一個(gè)很自然的想法是為了復(fù)用父類的代碼而繼承。但是實(shí)踐發(fā)現(xiàn),用繼承關(guān)系來(lái)實(shí)現(xiàn)軟件的復(fù)用有很多缺點(diǎn),一般來(lái)說(shuō)更為合理的方式是,用多個(gè)對(duì)象的組合關(guān)系來(lái)實(shí)現(xiàn)復(fù)用。
+ 繼承關(guān)系是子類“是一個(gè)”父類的關(guān)系,但如果是為了復(fù)用父類的已有功能來(lái)實(shí)現(xiàn)子類的新功能,常常會(huì)違反里氏替換原則。
+ 組合關(guān)系更容易處理有多個(gè)可復(fù)用模塊的情況。多重繼承會(huì)導(dǎo)致結(jié)構(gòu)復(fù)雜不易維護(hù)。
+ 組合關(guān)系更靈活易擴(kuò)展,只要使用適當(dāng)?shù)脑O(shè)計(jì)模式,使用者和被使用者都可被修改、擴(kuò)展、替換。
+ 組合關(guān)系可以提供運(yùn)行時(shí)的靈活性??梢栽谶\(yùn)行時(shí)指定一個(gè)模塊的底層實(shí)現(xiàn),或者運(yùn)行時(shí)替換一個(gè)對(duì)象的內(nèi)部實(shí)現(xiàn)。
為了體現(xiàn)它的重要性,這里我們看一個(gè)具體的例子。
我們知道,隊(duì)列是一種先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)。在隊(duì)列上可以執(zhí)行添加和刪除一個(gè)元素的操作,添加元素稱為入隊(duì),刪除元素稱為出隊(duì),并且元素出隊(duì)的順序與入隊(duì)的順序相同。顯然,隊(duì)列可以用雙向鏈表來(lái)實(shí)現(xiàn),那么,我們要不要把隊(duì)列設(shè)計(jì)成雙向鏈表的子類呢?
咋一看,可以讓queue私有繼承l(wèi)ist,隱藏掉list所有的方法,然后實(shí)現(xiàn)隊(duì)列的push方法調(diào)用list的push_pack方法,隊(duì)列的pop方法調(diào)用list的pop_front方法。非常簡(jiǎn)單直接。
但是,這種實(shí)現(xiàn)方式是有問(wèn)題的。到底啥問(wèn)題?一言兩語(yǔ)也講不清楚,你自己想去吧。
因此,C++和Java的標(biāo)準(zhǔn)庫(kù)都沒(méi)有采用這種繼承的方式實(shí)現(xiàn)隊(duì)列。
在C++的stl中,queue被設(shè)計(jì)成一個(gè)容器適配器。只要是是實(shí)現(xiàn)了push_back、pop_front的容器,都可以作為queue的底層容器。stl中就提供了2種可以套用queue的容器,是list和deque。list就是雙向鏈表。deque的實(shí)現(xiàn)是數(shù)組指針的數(shù)組,與list相比減少了內(nèi)存分配的次數(shù)。
在JDK中,Queue是一個(gè)interface,實(shí)現(xiàn)了Queue接口的有LinkedList、ArrayDeque、ConcurrentLinkedQueue、LinkedBlockingQueue等許多具體類。
為了體現(xiàn)它的重要性,這里我將用一個(gè)實(shí)例來(lái)加深你對(duì)它的印象。如果設(shè)計(jì)一個(gè)網(wǎng)絡(luò)組件庫(kù),HttpConnection應(yīng)該繼承TcpConnection嗎?
HttpConnection不再能夠提供符合TcpConnection的功能,不能當(dāng)作TcpConnection使用。考慮read方法,若直接暴露TcpConnection的read方法,則破壞內(nèi)部結(jié)構(gòu);若提供基于HTTP協(xié)議的read方法,又無(wú)法做到功能跟父類一致。
Http協(xié)議能夠使用不同的下層協(xié)議,例如TCPv6。繼承自TcpConnection就失去了這種擴(kuò)展性。
如果設(shè)計(jì)另一個(gè)類"HttpOverTcp6Connection",會(huì)導(dǎo)致二者有大量的重復(fù)代碼,而這些代碼恰恰是實(shí)現(xiàn)HTTP協(xié)議本身的功能,應(yīng)復(fù)用為好。
如果希望一個(gè)程序在IPv4和IPv6網(wǎng)絡(luò)下都可使用,需要做很多的工作來(lái)實(shí)現(xiàn)在運(yùn)行時(shí)(而非編譯時(shí))根據(jù)配置文件或用戶輸入選擇HttpConnection或HttpOverTcp6Connection。
繼承關(guān)系表達(dá)類的對(duì)外提供的功能,而非類的內(nèi)部實(shí)現(xiàn)。Java中HttpURLConnection繼承URLConnection,與之并列的是JarURLConnection,二者都提供了根據(jù)URL建立連接并通信的功能。
**下面以2個(gè)常用的設(shè)計(jì)模式為例,說(shuō)明它們的應(yīng)用場(chǎng)景和應(yīng)用價(jià)值,讓大家有一個(gè)比較直觀具體的感受。**
## 單例模式
單例模式是指,某個(gè)類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只能創(chuàng)建單個(gè)對(duì)象。單例模式最簡(jiǎn)單的設(shè)計(jì)模式,也是最容易用錯(cuò)的設(shè)計(jì)模式。
### 如何實(shí)現(xiàn)單例模式
單例模式非常簡(jiǎn)單,這個(gè)模式中只包含一個(gè)類。實(shí)現(xiàn)單例模式的重點(diǎn)是管理單例實(shí)例的創(chuàng)建。
+ C++,可以通過(guò)static局部變量的方式,也可以通過(guò)static指針成員變量的條件創(chuàng)建方式做到(即每次GetInstance的時(shí)候判空,如果為空則new,否則直接返回)。Java可以用static指針成員變量的方式。
+ 通常為了避免使用者錯(cuò)誤創(chuàng)建多余的對(duì)象,單例的構(gòu)造函數(shù)和析構(gòu)函數(shù)聲明為私有函數(shù)。
+ 多線程環(huán)境下,創(chuàng)建單例的代碼需要謹(jǐn)慎處理并發(fā)的問(wèn)題。一般做法是雙重檢查加鎖(即每次判空的時(shí)候先判空一次,如果為空則加鎖再次判空)。C++的靜態(tài)局部變量可以保證線程安全,java要使用synchronized實(shí)現(xiàn)。
+ 多種單例,如果有依賴關(guān)系,需要仔細(xì)處理構(gòu)建順序。C++的靜態(tài)局部變量在程序首次運(yùn)行到變量聲明處時(shí)執(zhí)行其構(gòu)造函數(shù)。Java的靜態(tài)變量初始化發(fā)生在類被加載時(shí)。
### 單例模式的好處
+ 使用簡(jiǎn)單,任何需要用到類實(shí)例的地方,直接用類的GetInstance()方法就便利的獲取到實(shí)例。
+ 可以避免使用全局變量,讓開發(fā)者有更好的OOP感,且可以讓程序員更好地控制初始化順序。
+ 它隱藏了對(duì)象的構(gòu)建細(xì)節(jié),且能避免多次構(gòu)建引起的錯(cuò)誤。
### 單例模式的探討
從原則上說(shuō),一個(gè)類應(yīng)努力提供它應(yīng)有的功能,而不應(yīng)對(duì)它的使用者做出過(guò)多限制。而單例模式限制這個(gè)類的對(duì)象只存在唯一實(shí)例。因此單例模式只應(yīng)在確有必要的情況下使用:
+ 技術(shù)上必須保證此對(duì)象全局唯一,例如代表應(yīng)用本身、對(duì)象管理器、全局服務(wù)等。
+ 程序中多處依賴此對(duì)象,采用單例模式能使代碼得到極大簡(jiǎn)化,例如全局配置選項(xiàng)。
要避免根據(jù)一時(shí)的具體需求將某類設(shè)計(jì)為單例,而極大地限制了可擴(kuò)展性。例如一個(gè)選課系統(tǒng)如果把學(xué)校信息設(shè)計(jì)為單例,將來(lái)想要支持跨校選課時(shí)就比較困難。
尤其注意,一旦某個(gè)類設(shè)計(jì)為單例,就會(huì)形成在程序各處隨意地引用這個(gè)對(duì)象的一種傾向。這正是單例模式的便利之處,但如果并不希望一個(gè)類有如此廣泛的耦合關(guān)系,則應(yīng)避免將其設(shè)計(jì)為單例。
此外,由這種便利性會(huì)引發(fā)更不利的傾向。在未經(jīng)仔細(xì)設(shè)計(jì)的系統(tǒng)中,隨著需求變更和系統(tǒng)演進(jìn),單例類可能會(huì)無(wú)節(jié)制地?cái)U(kuò)展,包含各種難以歸類的數(shù)據(jù)成員和各個(gè)模塊的中轉(zhuǎn)方法。
### 替代方案
通常有以下方法可以避免使用單例模式:
+ 享元模式。例如Android SDK使用activity.getApplication() ,避免“Application.getSingleton() ”。這樣取得Application實(shí)例并不像單例模式那么方便,從而限制了Application的耦合性。而通過(guò)Activity獲取Application是符合邏輯的設(shè)計(jì),大多數(shù)真正需要用到Application的場(chǎng)合并不影響使用。
+ 靜態(tài)方法。例如Unity引擎的物體查詢接口是GameObject.Find(name) ,而不是由比如“GameObjectManager”的單例類提供。靜態(tài)方法只提供單一的功能,并且調(diào)用時(shí)的寫法比單例模式更加簡(jiǎn)潔。但須注意,只有邏輯上與某個(gè)類有緊密聯(lián)系的功能才適合作為靜態(tài)方法。靜態(tài)方法如果濫用,會(huì)導(dǎo)致軟件結(jié)構(gòu)實(shí)際上變成了面向過(guò)程的設(shè)計(jì)。
## 觀察者模式
觀察者模式,當(dāng)一個(gè)對(duì)象發(fā)生改變時(shí),把這種改變通知給其他多個(gè)對(duì)象,從而影響其他對(duì)象的行為。又稱訂閱模式、事件模式等。
### 觀察者模式的組成
觀察者模式中包含兩個(gè)角色:
+ 被觀察者,它維護(hù)觀察者列表,并在自身發(fā)生改變時(shí)通知觀察者。也可稱為發(fā)布者、事件源等。
+ 觀察者,它將自身注冊(cè)到被觀察者維護(hù)的觀察者列表,并在接收到被觀察者的通知時(shí)做出響應(yīng)。觀察者也稱訂閱者。
### 如何實(shí)現(xiàn)觀察者模式
被觀察者的接口應(yīng)包含3個(gè)方法:增加觀察者、刪除觀察者、向觀察者發(fā)送通知。其中,增加觀察者、刪除觀察者通常由觀察者調(diào)用,用于表明哪些觀察者對(duì)象需要得到通知。發(fā)送通知方法通常由被觀察者調(diào)用,因此可以考慮定義為protected方法。發(fā)送通知方法應(yīng)遍歷自身的觀察者列表,逐一調(diào)用觀察者的接收通知方法。這3個(gè)方法功能較為明確,可以用抽象類、模板、泛型等技術(shù)提供通用實(shí)現(xiàn)。
觀察者的接口需要提供接收通知方法,以供被觀察者調(diào)用。不同的具體觀察者類型實(shí)現(xiàn)各自的接收通知方法,實(shí)現(xiàn)當(dāng)被觀察者發(fā)生改變時(shí),觀察者應(yīng)做出的響應(yīng)。
由于觀察者接口只有一個(gè)方法,在C#語(yǔ)言中deligate來(lái)代替,在C++中可以用std::function代替,這樣進(jìn)一步解耦了不同類型的觀察者,其不必派生自同一個(gè)公共接口。當(dāng)然,當(dāng)系統(tǒng)中的觀察者的確有所聯(lián)系時(shí),則不應(yīng)該過(guò)度追求解耦,顯式定義一個(gè)觀察者接口或抽象類可以使結(jié)構(gòu)更為清晰、嚴(yán)謹(jǐn)。
觀察者模式常常與命令模式配合使用。命令模式是,將一個(gè)請(qǐng)求封裝為一個(gè)對(duì)象,使發(fā)出請(qǐng)求的責(zé)任和執(zhí)行請(qǐng)求的責(zé)任分割開。采用命令模式,將通知或事件封裝成對(duì)象,可以使觀察者和被觀察者之間進(jìn)一步解耦。例如,如果不希望在被觀察者的運(yùn)行過(guò)程中穿插執(zhí)行觀察者的函數(shù),則可以保存命令稍后執(zhí)行。
### 觀察者模式的特點(diǎn)和適用場(chǎng)景
每種設(shè)計(jì)模式都有其最適合的應(yīng)用場(chǎng)景,如果正確使用,可以幫助理清復(fù)雜的耦合關(guān)系,簡(jiǎn)化設(shè)計(jì)。但如果在不合適的場(chǎng)景中生搬硬套,則會(huì)把原本簡(jiǎn)單的事情搞復(fù)雜,并不能真正解決需求。觀察者模式也不例外,在實(shí)際項(xiàng)目中,必須具體問(wèn)題具體分析,考察需求是否符合觀察者模式的特點(diǎn),決定是否選用觀察者模式。
+ 觀察者模式適合一對(duì)多的關(guān)聯(lián)關(guān)系。一個(gè)被觀察者可以有零個(gè)或多個(gè)觀察者。當(dāng)然,一個(gè)程序中被觀察者可以有多個(gè),每個(gè)被觀察者都有自己的一對(duì)多關(guān)系,而相互之間沒(méi)有關(guān)聯(lián)。
+ 邏輯上的依賴關(guān)系是單向的。被觀察者往往可以獨(dú)立運(yùn)行,并不依賴觀察者。而觀察者的順利運(yùn)行依賴于被觀察者的推動(dòng),離開被觀察者就運(yùn)行不起來(lái)了。
+ 調(diào)用關(guān)系與邏輯關(guān)系是反向的。邏輯上被觀察者不依賴觀察者,但有事件發(fā)生時(shí)卻是被觀察者調(diào)用了觀察者的方法。
下面我們用一個(gè)例子來(lái)看如何應(yīng)用觀察者模式來(lái)解決具體的需求,以及使用觀察者模式帶來(lái)的好處。
我們假設(shè)需求是這樣:某個(gè)應(yīng)用程序中有多處要用到定時(shí)執(zhí)行的功能,就是到一個(gè)固定的時(shí)間需要執(zhí)行一個(gè)特定的函數(shù)。很自然,多處要用到的功能應(yīng)該提煉出來(lái)作為一個(gè)子模塊。但另一方面,我們又不希望這個(gè)定時(shí)模塊與每一個(gè)用到了定時(shí)功能的其他模塊都有很強(qiáng)的耦合。
觀察者模式可以幫助我們?cè)O(shè)計(jì)定時(shí)模塊,既能服用,又有低耦合性。這里我們的示例實(shí)現(xiàn)如下。為了突出展示觀察者模式,我對(duì)需求做了一定簡(jiǎn)化,我們的定時(shí)模塊固定在每天上午9點(diǎn)觸發(fā),不支持自定義時(shí)間。
+ [C++語(yǔ)言實(shí)現(xiàn)AlarmClock](AlarmClock.c++)
- #include <iostream>
- #include <list>
- //簡(jiǎn)單鬧鐘,每天早上9點(diǎn)響
- class AlarmClock {
- public:
- class Alarm {
- public:
- virtual ~Alarm() {}
- virtual void onClockAlarmed() = 0;
- };
- private:
- static const int TimeZone = 8; // 北京時(shí)間東8區(qū)
- static const int AlarmHour = 9;
- std::list<Alarm*> alarms;
- time_t tomorrow;
- public:
- AlarmClock() {
- //將tomorrow設(shè)置為明天9點(diǎn)鐘
- time_t now = time(0);
- tomorrow = now - now % 86400 - TimeZone * 3600 + AlarmHour * 3600;
- if (tomorrow < now)
- tomorrow += 86400;
- }
- AlarmClock(AlarmClock&) = delete;
- void setAlarm(Alarm* alarm) {
- alarms.push_back(alarm);
- }
- void unsetAlarm(Alarm* alarm) {
- alarms.remove(alarm);
- }
- void advance() {
- tomorrow += 86400;
- for (auto alarm : alarms) {
- alarm->onClockAlarmed();
- }
- }
- void update(time_t now) {
- while (now >= tomorrow) {
- advance();
- }
- }
- };
- // 資深程序員張三
- class TestZhangSan : public AlarmClock::Alarm {
- public:
- ~TestZhangSan() {}
- TestZhangSan(AlarmClock& clock) {
- clock.setAlarm(this);
- }
- // 開始了996的一天
- void onClockAlarmed() {
- std::cout << "Zhang San is going to work..." << std::endl;
- }
- };
- // 隔壁上夜班的王叔叔
- class TestLaoWang : public AlarmClock::Alarm {
- public:
- ~TestLaoWang(){}
- TestLaoWang(AlarmClock& clock) {
- clock.setAlarm(this);
- }
- // 下班回家睡覺(jué)
- void onClockAlarmed() {
- std::cout << "Lao Wang is going to bed..." << std::endl;
- }
- };
- int main(int argc, char **argv)
- {
- AlarmClock clock;
- TestZhangSan zhang(clock);
- TestLaoWang wang(clock);
- time_t now = time(0);
- now -= now % 3600;
- for (int i = 0; i < 24; i++) {
- std::cout << "Now:" << ctime(&now);
- clock.update(now);
- now += 3600;
- }
- return 0;
- }
+ [Java語(yǔ)言實(shí)現(xiàn)AlarmClock](AlarmClock.java)
- import java.util.Calendar;
- import java.util.List;
- import java.util.LinkedList;
- //簡(jiǎn)單鬧鐘,每天早上9點(diǎn)響
- public class AlarmClock {
- public static interface Alarm {
- void onClockAlarmed();
- }
- private static final int AlarmHour = 9;
- private final List<Alarm*> alarms = new LinkedList<>();
- private Calendar tomorrow;
- public AlarmClock() {
- //將tomorrow設(shè)置為明天9點(diǎn)鐘
- tomorrow = Calendar.getInstance();
- boolean addDay = tomorrow.get(Calendar.HOUR_OF_DAY) >= AlarmHour;
- tomorrow.set(Calendar.HOUR_OF_DAY, AlarmHour);
- tomorrow.set(Calendar.MINUTE, 0);
- tomorrow.set(Calendar.SECOND, 0);
- tomorrow.set(Calendar.MILLISECOND, 0);
- if (addDay) {
- tomorrow.add(Calendar.DAY_OF_MONTH, 1);
- }
- }
- public void setAlarm(Alarm alarm) {
- alarms.add(alarm);
- }
- public void unsetAlarm(Alarm alarm) {
- alarms.remove(alarm);
- }
- public void advance() {
- tomorrow += 86400;
- for (Alarm alarm : alarms) {
- alarm.onClockAlarmed();
- }
- }
- public void update(Calendar now) {
- while (now >= tomorrow) {
- advance();
- }
- }
- // 資深程序員張三
- private class TestZhangSan : public Alarm {
- public:
- TestZhangSan(AlarmClock& clock) {
- clock.setAlarm(this);
- }
- // 開始了996的一天
- public void onClockAlarmed() {
- System.out.println("Zhang San is going to work...");
- }
- }
- // 隔壁上夜班的老王
- private class TestLaoWang : public Alarm {
- public TestLaoWang(AlarmClock& clock) {
- clock.setAlarm(this);
- }
- // 下班回家睡覺(jué)
- public void onClockAlarmed() {
- System.out.println("Lao Wang is going to bed...");
- }
- }
- public static void main(String []args){
- AlarmClock clock = new AlarmClock();
- TestZhangSan zhang = new TestZhangSan(clock);
- TestLaoWang wang = new TestLaoWang(clock);
- Calendar now = Calendar.getInstance();
- now.set(Calendar.MINUTE, 0);
- now.set(Calendar.SECOND, 0);
- now.set(Calendar.MILLISECOND, 0);
- //假裝時(shí)間經(jīng)過(guò)了24小時(shí)
- for (int i = 0; i < 24; i++) {
- System.out.println("Now:" + now.getTime());
- clock.update(now);
- now.add(Calendar.HOUR_OF_DAY, 1);
- }
- }
- }
在這個(gè)例子中,AlarmClock類是被觀察者,Alarm接口及其具體子類是觀察者。按照觀察者模式,被觀察者AlarmClock維護(hù)了它的觀察者的列表。當(dāng)時(shí)間進(jìn)行到新一天的早晨,AlarmClock的狀態(tài)發(fā)生變化,也就是產(chǎn)生了一個(gè)事件,這時(shí)AlarmClock調(diào)用每個(gè)Alarm的方法。這樣,Alarm的具體子類對(duì)象,即每個(gè)希望定時(shí)執(zhí)行的模塊,就能夠在正確的時(shí)間得到執(zhí)行。
由于采用了觀察者模式,AlarmClock與其它模塊之間只通過(guò)Alarm接口交互,AlarmClock只引用Alarm,而不需要關(guān)心每個(gè)Alarm到底是哪個(gè)具體類,也不關(guān)心調(diào)用Alarm后究竟會(huì)執(zhí)行哪些操作。如果Alarm的具體子類需要修改,我們并不需要修改AlarmClock類。如果有新的模塊需要用到定時(shí)功能,只需要讓新模塊實(shí)現(xiàn)Alarm接口即可。這就是觀察者模式降低耦合性的作用。
因?yàn)檫@個(gè)例子中被觀察者只有一個(gè),因此被觀察者的抽象接口被省略了。并且我們沒(méi)有使用Observer、Subject等非常寬泛的名字,而是結(jié)合實(shí)際情況,觀察目標(biāo)就是具體類AlarmClock類,觀察者被稱為Alarm。這樣使得整個(gè)設(shè)計(jì)非常自然,沒(méi)有生搬硬套設(shè)計(jì)模式的痕跡,哪怕是沒(méi)有學(xué)過(guò)設(shè)計(jì)模式的人也能夠看懂。這就是在具體應(yīng)用設(shè)計(jì)模式時(shí)常常應(yīng)該做的剪裁和調(diào)整。
需要指出,這個(gè)例子是為了能夠清晰演示觀察者模式而專門假設(shè)的場(chǎng)景。你可以嘗試把例子進(jìn)行擴(kuò)展。如果希望支持為每個(gè)Alarm指定不同的執(zhí)行時(shí)間,應(yīng)如何設(shè)計(jì)?如果張三多件事情需要分別定時(shí)執(zhí)行,又應(yīng)如何設(shè)計(jì)?
在實(shí)際項(xiàng)目中,業(yè)務(wù)需求一定會(huì)更為復(fù)雜,工程師需要在復(fù)雜需求中識(shí)別出在哪里使用哪種設(shè)計(jì)模式能夠帶來(lái)好處,這是需要鍛煉提升的能力。實(shí)際項(xiàng)目的設(shè)計(jì)也會(huì)根據(jù)需求做出更多的調(diào)整,多一些類或少一些類,常??雌饋?lái)跟最初學(xué)習(xí)設(shè)計(jì)模式時(shí)看到的很不一樣。因此學(xué)習(xí)設(shè)計(jì)模式重在掌握思想,不能生搬硬套。無(wú)招勝有招。
## 總結(jié)
短短一篇文章,想要講清設(shè)計(jì)模式的所有內(nèi)容幾乎是不可能完成的任務(wù),所以我沒(méi)有逐一講解,而是結(jié)合我自己工作中遇到過(guò)的問(wèn)題,來(lái)帶你重新認(rèn)識(shí)設(shè)計(jì)模式,為你樹立它的重要性的觀念,避免陷入細(xì)節(jié)泥潭,歡樂(lè)的時(shí)間過(guò)太快,又是時(shí)候說(shuō)拜拜,最后,恭喜大家,你已經(jīng)掌握了設(shè)計(jì)模式,去干一番對(duì)人類有益的事業(yè)吧。
本篇由御姐供稿,版權(quán)和解釋權(quán)歸御姐所有,文章內(nèi)容代表御姐意見,本農(nóng)夫自媒體對(duì)文章觀點(diǎn)不持立場(chǎng)。
本文轉(zhuǎn)載自微信公眾號(hào)「碼磚雜役」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼磚雜役公眾號(hào)。