新聞中心
御姐力作,深入淺出,妙趣橫生,值得一看!

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


咨詢
建站咨詢
