前言
無聊的時候,也去QQ游戲大廳玩五子棋或者象棋;作為程序員,看到一個產(chǎn)品,總要去想想它是怎么設(shè)計的,怎么完成的,我想這個是所有程序員都會做的事情吧(強迫癥???)。有的時候,想完了,還要做一個DEMO出來,才能體現(xiàn)自己的NB,然后還有點小成就感。
在玩五子棋或象棋的時候,我就想過,騰訊那幫伙計是怎么做的呢?五子棋的棋子有黑白兩色,難道每次放一個棋子就new一個對象么?象棋有車、馬、相、士、帥、炮和兵,是不是每盤棋都要把所有的棋子都new出來呢?如果真的是每一個棋子都new一個,那么再加上那么多人玩;那要new多少對象啊,如果是這樣做的話,我想有多少服務(wù)器都是搞不定的,可能QQ游戲大廳會比12306還糟糕。那騰訊那幫伙計是如何實現(xiàn)的呢?那就要說到今天總結(jié)的享元模式了。
什么是享元模式?
在GOF的《設(shè)計模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書中對享元模式是這樣說的:運用共享技術(shù)有效地支持大量細(xì)粒度的對象。
就如上面說的棋子,如果每個棋子都new一個對象,就會存在大量細(xì)粒度的棋子對象,這對服務(wù)器的內(nèi)存空間是一種考驗,也是一種浪費。我們都知道,比如我在2013號房間和別人下五子棋,2014號房間也有人在下五子棋,并不會因為我在2013號房間,而別人在2014號房間,而導(dǎo)致我們的棋子是不一樣的。這就是說,2013號房間和2014號房間的棋子都是一樣的,所有的五子棋房間的棋子都是一樣的。唯一的不同是每個棋子在不同的房間的不同棋盤的不同位置上。所以,對于棋子來說,我們不用放一個棋子就new一個棋子對象,只需要在需要的時候,去請求獲得對應(yīng)的棋子對象,如果沒有,就new一個棋子對象;如果有了,就直接返回棋子對象。這里以五子棋為例子,進(jìn)行分析,當(dāng)玩家在棋盤上放入第一個白色棋子時,此時由于沒有白色棋子,所以就new一個白色棋子;當(dāng)另一個玩家放入第一個黑色棋子時,此時由于沒有黑色棋子,所以就需要new一個黑色棋子;當(dāng)玩家再次放入一個白色棋子時,就去查詢是否有已經(jīng)存在的白色棋子對象,由于第一次已經(jīng)new了一個白色棋子對象,所以,現(xiàn)在不會再次new一個白色棋子對象,而是返回以前new的白色棋子對象;對于黑色棋子,亦是同理;獲得了棋子對象,我們只需要設(shè)置棋子的不同棋盤位置即可。
UML類圖
Flyweight:描述一個接口,通過這個接口flyweight可以接受并作用于外部狀態(tài);
ConcreteFlyweight:實現(xiàn)Flyweight接口,并為定義了一些內(nèi)部狀態(tài),ConcreteFlyweight對象必須是可共享的;同時,它所存儲的狀態(tài)必須是內(nèi)部的;即,它必須獨立于ConcreteFlyweight對象的場景;
UnsharedConcreteFlyweight:并非所有的Flyweight子類都需要被共享。Flyweight接口使共享成為可能,但它并不強制共享。
FlyweightFactory:創(chuàng)建并管理flyweight對象。它需要確保合理地共享flyweight;當(dāng)用戶請求一個flyweight時,F(xiàn)lyweightFactory對象提供一個已創(chuàng)建的實例,如果請求的實例不存在的情況下,就新創(chuàng)建一個實例;
Client:維持一個對flyweight的引用;同時,它需要計算或存儲flyweight的外部狀態(tài)。
實現(xiàn)要點
根據(jù)我們的經(jīng)驗,當(dāng)要將一個對象進(jìn)行共享時,就需要考慮到對象的狀態(tài)問題了;不同的客戶端獲得共享的對象之后,可能會修改共享對象的某些狀態(tài);大家都修改了共享對象的狀態(tài),那么就會出現(xiàn)對象狀態(tài)的紊亂。對于享元模式,在實現(xiàn)時一定要考慮到共享對象的狀態(tài)問題。那么享元模式是如何實現(xiàn)的呢?
在享元模式中,有兩個非常重要的概念:內(nèi)部狀態(tài)和外部狀態(tài)。
內(nèi)部狀態(tài)存儲于flyweight中,它包含了獨立于flyweight場景的信息,這些信息使得flyweight可以被共享。而外部狀態(tài)取決于flyweight場景,并根據(jù)場景而變化,因此不可共享。用戶對象負(fù)責(zé)在必要的時候?qū)⑼獠繝顟B(tài)傳遞給flyweight。
flyweight執(zhí)行時所需的狀態(tài)必定是內(nèi)部的或外部的。內(nèi)部狀態(tài)存儲于ConcreteFlyweight對象之中;而外部對象則由Client對象存儲或計算。當(dāng)用戶調(diào)用flyweight對象的操作時,將該狀態(tài)傳遞給它。同時,用戶不應(yīng)該直接對ConcreteFlyweight類進(jìn)行實例化,而只能從FlyweightFactory對象得到ConcreteFlyweight對象,這可以保證對它們適當(dāng)?shù)剡M(jìn)行共享;由于共享一個實例,所以在創(chuàng)建這個實例時,就可以考慮使用單例模式來進(jìn)行實現(xiàn)。
享元模式的工廠類維護(hù)了一個實例列表,這個列表中保存了所有的共享實例;當(dāng)用戶從享元模式的工廠類請求共享對象時,首先查詢這個實例表,如果不存在對應(yīng)實例,則創(chuàng)建一個;如果存在,則直接返回對應(yīng)的實例。
代碼實現(xiàn):
內(nèi)部狀態(tài)包括棋子的顏色,外部狀態(tài)包括棋子在棋盤上的位置。最終,我們省去了多個實例對象存儲棋子顏色的空間,從而達(dá)到了空間的節(jié)約。
在上面的代碼中,我建立了一個CCheseboard用于表示棋盤,棋盤類中保存了放置的黑色棋子和白色棋子;這就相當(dāng)于在外部保存了共享對象的外部狀態(tài);對于棋盤對象,我們是不是又可以使用享元模式呢?再設(shè)計一個棋局類進(jìn)行管理棋盤上的棋子布局,用來保存外部狀態(tài)。對于這個,這里不進(jìn)行討論了。
優(yōu)點
享元模式可以避免大量非常相似對象的開銷。在程序設(shè)計時,有時需要生成大量細(xì)粒度的類實例來表示數(shù)據(jù)。如果能發(fā)現(xiàn)這些實例數(shù)據(jù)除了幾個參數(shù)外基本都是相同的,使用享元模式就可以大幅度地減少對象的數(shù)量。
使用場合
Flyweight模式的有效性很大程度上取決于如何使用它以及在何處使用它。當(dāng)以下條件滿足時,我們就可以使用享元模式了。
1.一個應(yīng)用程序使用了大量的對象;
2.完全由于使用大量的對象,造成很大的存儲開銷;
3.對象的大多數(shù)狀態(tài)都可變?yōu)橥獠繝顟B(tài);
4.如果刪除對象的外部狀態(tài),那么可以用相對較少的共享對象取代很多組對象。
擴展
之前總結(jié)了組合模式組合模式,現(xiàn)在回過頭來看看,享元模式就好比在組合模式的基礎(chǔ)上加上了一個工廠類,進(jìn)行共享控制。是的,組合模式有的時候會產(chǎn)生很多細(xì)粒度的對象,很多時候,我們會將享元模式和組合模式進(jìn)行結(jié)合使用。
總結(jié)
使用享元模式可以避免大量相似對象的開銷,減小了空間消耗;而空間的消耗是由以下幾個因素決定的:
1.實例對象減少的數(shù)目;
2.對象內(nèi)部狀態(tài)的數(shù)目;對象內(nèi)部狀態(tài)越多,消耗的空間也會越少;
3.外部狀態(tài)是計算的還是存儲的;由于外部狀態(tài)可能需要存儲,如果外部狀態(tài)存儲起來,那么空間的節(jié)省就不會太多。
共享的Flyweight越多,存儲節(jié)約也就越多,節(jié)約量隨著共享狀態(tài)的增多而增大。當(dāng)對象使用大量的內(nèi)部及外部狀態(tài),并且外部狀態(tài)是計算出來的而非存儲的時候,節(jié)約量將達(dá)到最大。所以,可以使用兩種方法來節(jié)約存儲:用共享減少內(nèi)部狀態(tài)的消耗;用計算時間換取對外部狀態(tài)的存儲。
同時,在實現(xiàn)的時候,一定要控制好外部狀態(tài)與共享對象的對應(yīng)關(guān)系,比如我在代碼實現(xiàn)部分,在CCheseboard類中使用了一個map進(jìn)行彼此之間的映射,這個映射在實際開發(fā)中需要考慮的。
好了,享元模式就總結(jié)到這里了。希望大家和我分享你對設(shè)計模式的理解。我堅信:分享使我們更進(jìn)步。
PS:至于騰訊那幫伙計到底是如何實現(xiàn)QQ游戲大廳的,我也不知道,這里也完全是猜測的,請不要以此為基準(zhǔn)。
新聞熱點
疑難解答