C++設(shè)計(jì)模式之享元模式
前言
無(wú)聊的時(shí)候,也去QQ游戲大廳玩五子棋或者象棋;作為程序員,看到一個(gè)產(chǎn)品,總要去想想它是怎么設(shè)計(jì)的,怎么完成的,我想這個(gè)是所有程序員都會(huì)做的事情吧(強(qiáng)迫癥???)。有的時(shí)候,想完了,還要做一個(gè)DEMO出來(lái),才能體現(xiàn)自己的NB,然后還有點(diǎn)小成就感。
在玩五子棋或象棋的時(shí)候,我就想過,騰訊那幫伙計(jì)是怎么做的呢?五子棋的棋子有黑白兩色,難道每次放一個(gè)棋子就new一個(gè)對(duì)象么?象棋有車、馬、相、士、帥、炮和兵,是不是每盤棋都要把所有的棋子都new出來(lái)呢?如果真的是每一個(gè)棋子都new一個(gè),那么再加上那么多人玩;那要new多少對(duì)象啊,如果是這樣做的話,我想有多少服務(wù)器都是搞不定的,可能QQ游戲大廳會(huì)比12306還糟糕。那騰訊那幫伙計(jì)是如何實(shí)現(xiàn)的呢?那就要說到今天總結(jié)的享元模式了。
什么是享元模式?
在GOF的《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書中對(duì)享元模式是這樣說的:運(yùn)用共享技術(shù)有效地支持大量細(xì)粒度的對(duì)象。
就如上面說的棋子,如果每個(gè)棋子都new一個(gè)對(duì)象,就會(huì)存在大量細(xì)粒度的棋子對(duì)象,這對(duì)服務(wù)器的內(nèi)存空間是一種考驗(yàn),也是一種浪費(fèi)。我們都知道,比如我在2013號(hào)房間和別人下五子棋,2014號(hào)房間也有人在下五子棋,并不會(huì)因?yàn)槲以?013號(hào)房間,而別人在2014號(hào)房間,而導(dǎo)致我們的棋子是不一樣的。這就是說,2013號(hào)房間和2014號(hào)房間的棋子都是一樣的,所有的五子棋房間的棋子都是一樣的。唯一的不同是每個(gè)棋子在不同的房間的不同棋盤的不同位置上。所以,對(duì)于棋子來(lái)說,我們不用放一個(gè)棋子就new一個(gè)棋子對(duì)象,只需要在需要的時(shí)候,去請(qǐng)求獲得對(duì)應(yīng)的棋子對(duì)象,如果沒有,就new一個(gè)棋子對(duì)象;如果有了,就直接返回棋子對(duì)象。這里以五子棋為例子,進(jìn)行分析,當(dāng)玩家在棋盤上放入第一個(gè)白色棋子時(shí),此時(shí)由于沒有白色棋子,所以就new一個(gè)白色棋子;當(dāng)另一個(gè)玩家放入第一個(gè)黑色棋子時(shí),此時(shí)由于沒有黑色棋子,所以就需要new一個(gè)黑色棋子;當(dāng)玩家再次放入一個(gè)白色棋子時(shí),就去查詢是否有已經(jīng)存在的白色棋子對(duì)象,由于第一次已經(jīng)new了一個(gè)白色棋子對(duì)象,所以,現(xiàn)在不會(huì)再次new一個(gè)白色棋子對(duì)象,而是返回以前new的白色棋子對(duì)象;對(duì)于黑色棋子,亦是同理;獲得了棋子對(duì)象,我們只需要設(shè)置棋子的不同棋盤位置即可。
UML類圖
Flyweight:描述一個(gè)接口,通過這個(gè)接口flyweight可以接受并作用于外部狀態(tài);
ConcreteFlyweight:實(shí)現(xiàn)Flyweight接口,并為定義了一些內(nèi)部狀態(tài),ConcreteFlyweight對(duì)象必須是可共享的;同時(shí),它所存儲(chǔ)的狀態(tài)必須是內(nèi)部的;即,它必須獨(dú)立于ConcreteFlyweight對(duì)象的場(chǎng)景;
UnsharedConcreteFlyweight:并非所有的Flyweight子類都需要被共享。Flyweight接口使共享成為可能,但它并不強(qiáng)制共享。
FlyweightFactory:創(chuàng)建并管理flyweight對(duì)象。它需要確保合理地共享flyweight;當(dāng)用戶請(qǐng)求一個(gè)flyweight時(shí),F(xiàn)lyweightFactory對(duì)象提供一個(gè)已創(chuàng)建的實(shí)例,如果請(qǐng)求的實(shí)例不存在的情況下,就新創(chuàng)建一個(gè)實(shí)例;
Client:維持一個(gè)對(duì)flyweight的引用;同時(shí),它需要計(jì)算或存儲(chǔ)flyweight的外部狀態(tài)。
實(shí)現(xiàn)要點(diǎn)
根據(jù)我們的經(jīng)驗(yàn),當(dāng)要將一個(gè)對(duì)象進(jìn)行共享時(shí),就需要考慮到對(duì)象的狀態(tài)問題了;不同的客戶端獲得共享的對(duì)象之后,可能會(huì)修改共享對(duì)象的某些狀態(tài);大家都修改了共享對(duì)象的狀態(tài),那么就會(huì)出現(xiàn)對(duì)象狀態(tài)的紊亂。對(duì)于享元模式,在實(shí)現(xiàn)時(shí)一定要考慮到共享對(duì)象的狀態(tài)問題。那么享元模式是如何實(shí)現(xiàn)的呢?
在享元模式中,有兩個(gè)非常重要的概念:內(nèi)部狀態(tài)和外部狀態(tài)。
內(nèi)部狀態(tài)存儲(chǔ)于flyweight中,它包含了獨(dú)立于flyweight場(chǎng)景的信息,這些信息使得flyweight可以被共享。而外部狀態(tài)取決于flyweight場(chǎng)景,并根據(jù)場(chǎng)景而變化,因此不可共享。用戶對(duì)象負(fù)責(zé)在必要的時(shí)候?qū)⑼獠繝顟B(tài)傳遞給flyweight。
flyweight執(zhí)行時(shí)所需的狀態(tài)必定是內(nèi)部的或外部的。內(nèi)部狀態(tài)存儲(chǔ)于ConcreteFlyweight對(duì)象之中;而外部對(duì)象則由Client對(duì)象存儲(chǔ)或計(jì)算。當(dāng)用戶調(diào)用flyweight對(duì)象的操作時(shí),將該狀態(tài)傳遞給它。同時(shí),用戶不應(yīng)該直接對(duì)ConcreteFlyweight類進(jìn)行實(shí)例化,而只能從FlyweightFactory對(duì)象得到ConcreteFlyweight對(duì)象,這可以保證對(duì)它們適當(dāng)?shù)剡M(jìn)行共享;由于共享一個(gè)實(shí)例,所以在創(chuàng)建這個(gè)實(shí)例時(shí),就可以考慮使用單例模式來(lái)進(jìn)行實(shí)現(xiàn)。
享元模式的工廠類維護(hù)了一個(gè)實(shí)例列表,這個(gè)列表中保存了所有的共享實(shí)例;當(dāng)用戶從享元模式的工廠類請(qǐng)求共享對(duì)象時(shí),首先查詢這個(gè)實(shí)例表,如果不存在對(duì)應(yīng)實(shí)例,則創(chuàng)建一個(gè);如果存在,則直接返回對(duì)應(yīng)的實(shí)例。
代碼實(shí)現(xiàn):
#include <iostream>
#include <map>
#include <vector>
using namespace std;
typedef struct pointTag
{
int x;
int y;
pointTag(){}
pointTag(int a, int b)
{
x = a;
y = b;
}
bool operator <(const pointTag& other) const
{
if (x < other.x)
{
return true;
}
else if (x == other.x)
{
return y < other.y;
}
return false;
}
}POINT;
typedef enum PieceColorTag
{
BLACK,
WHITE
}PIECECOLOR;
class CPiece
{
public:
CPiece(PIECECOLOR color) : m_color(color){}
PIECECOLOR GetColor() { return m_color; }
// Set the external state
void SetPoint(POINT point) { m_point = point; }
POINT GetPoint() { return m_point; }
protected:
// Internal state
PIECECOLOR m_color;
// external state
POINT m_point;
};
class CGomoku : public CPiece
{
public:
CGomoku(PIECECOLOR color) : CPiece(color){}
};
class CPieceFactory
{
public:
CPiece *GetPiece(PIECECOLOR color)
{
CPiece *pPiece = NULL;
if (m_vecPiece.empty())
{
pPiece = new CGomoku(color);
m_vecPiece.push_back(pPiece);
}
else
{
bool bFound = false; // 非常感謝fireace指出的問題
for (vector<CPiece *>::iterator it = m_vecPiece.begin(); it != m_vecPiece.end(); ++it)
{
if ((*it)->GetColor() == color)
{
bFound = true;
pPiece = *it;
break;
}
bFound = false;
}
if (!bFound)
{
pPiece = new CGomoku(color);
m_vecPiece.push_back(pPiece);
}
}
return pPiece;
}
~CPieceFactory()
{
for (vector<CPiece *>::iterator it = m_vecPiece.begin(); it != m_vecPiece.end(); ++it)
{
if (*it != NULL)
{
delete *it;
*it = NULL;
}
}
}
private:
vector<CPiece *> m_vecPiece;
};
class CChessboard
{
public:
void Draw(CPiece *piece)
{
if (piece->GetColor())
{
cout<<"Draw a White"<<" at ("<<piece->GetPoint().x<<","<<piece->GetPoint().y<<")"<<endl;
}
else
{
cout<<"Draw a Black"<<" at ("<<piece->GetPoint().x<<","<<piece->GetPoint().y<<")"<<endl;
}
m_mapPieces.insert(pair<POINT, CPiece *>(piece->GetPoint(), piece));
}
void ShowAllPieces()
{
for (map<POINT, CPiece *>::iterator it = m_mapPieces.begin(); it != m_mapPieces.end(); ++it)
{
if (it->second->GetColor())
{
cout<<"("<<it->first.x<<","<<it->first.y<<") has a White chese."<<endl;
}
else
{
cout<<"("<<it->first.x<<","<<it->first.y<<") has a Black chese."<<endl;
}
}
}
private:
map<POINT, CPiece *> m_mapPieces;
};
int main()
{
CPieceFactory *pPieceFactory = new CPieceFactory();
CChessboard *pCheseboard = new CChessboard();
// The player1 get a white piece from the pieces bowl
CPiece *pPiece = pPieceFactory->GetPiece(WHITE);
pPiece->SetPoint(POINT(2, 3));
pCheseboard->Draw(pPiece);
// The player2 get a black piece from the pieces bowl
pPiece = pPieceFactory->GetPiece(BLACK);
pPiece->SetPoint(POINT(4, 5));
pCheseboard->Draw(pPiece);
// The player1 get a white piece from the pieces bowl
pPiece = pPieceFactory->GetPiece(WHITE);
pPiece->SetPoint(POINT(2, 4));
pCheseboard->Draw(pPiece);
// The player2 get a black piece from the pieces bowl
pPiece = pPieceFactory->GetPiece(BLACK);
pPiece->SetPoint(POINT(3, 5));
pCheseboard->Draw(pPiece);
/*......*/
//Show all cheses
cout<<"Show all cheses"<<endl;
pCheseboard->ShowAllPieces();
if (pCheseboard != NULL)
{
delete pCheseboard;
pCheseboard = NULL;
}
if (pPieceFactory != NULL)
{
delete pPieceFactory;
pPieceFactory = NULL;
}
}
內(nèi)部狀態(tài)包括棋子的顏色,外部狀態(tài)包括棋子在棋盤上的位置。最終,我們省去了多個(gè)實(shí)例對(duì)象存儲(chǔ)棋子顏色的空間,從而達(dá)到了空間的節(jié)約。
在上面的代碼中,我建立了一個(gè)CCheseboard用于表示棋盤,棋盤類中保存了放置的黑色棋子和白色棋子;這就相當(dāng)于在外部保存了共享對(duì)象的外部狀態(tài);對(duì)于棋盤對(duì)象,我們是不是又可以使用享元模式呢?再設(shè)計(jì)一個(gè)棋局類進(jìn)行管理棋盤上的棋子布局,用來(lái)保存外部狀態(tài)。對(duì)于這個(gè),這里不進(jìn)行討論了。
優(yōu)點(diǎn)
享元模式可以避免大量非常相似對(duì)象的開銷。在程序設(shè)計(jì)時(shí),有時(shí)需要生成大量細(xì)粒度的類實(shí)例來(lái)表示數(shù)據(jù)。如果能發(fā)現(xiàn)這些實(shí)例數(shù)據(jù)除了幾個(gè)參數(shù)外基本都是相同的,使用享元模式就可以大幅度地減少對(duì)象的數(shù)量。
使用場(chǎng)合
Flyweight模式的有效性很大程度上取決于如何使用它以及在何處使用它。當(dāng)以下條件滿足時(shí),我們就可以使用享元模式了。
1.一個(gè)應(yīng)用程序使用了大量的對(duì)象;
2.完全由于使用大量的對(duì)象,造成很大的存儲(chǔ)開銷;
3.對(duì)象的大多數(shù)狀態(tài)都可變?yōu)橥獠繝顟B(tài);
4.如果刪除對(duì)象的外部狀態(tài),那么可以用相對(duì)較少的共享對(duì)象取代很多組對(duì)象。
擴(kuò)展
之前總結(jié)了組合模式組合模式,現(xiàn)在回過頭來(lái)看看,享元模式就好比在組合模式的基礎(chǔ)上加上了一個(gè)工廠類,進(jìn)行共享控制。是的,組合模式有的時(shí)候會(huì)產(chǎn)生很多細(xì)粒度的對(duì)象,很多時(shí)候,我們會(huì)將享元模式和組合模式進(jìn)行結(jié)合使用。
總結(jié)
使用享元模式可以避免大量相似對(duì)象的開銷,減小了空間消耗;而空間的消耗是由以下幾個(gè)因素決定的:
1.實(shí)例對(duì)象減少的數(shù)目;
2.對(duì)象內(nèi)部狀態(tài)的數(shù)目;對(duì)象內(nèi)部狀態(tài)越多,消耗的空間也會(huì)越少;
3.外部狀態(tài)是計(jì)算的還是存儲(chǔ)的;由于外部狀態(tài)可能需要存儲(chǔ),如果外部狀態(tài)存儲(chǔ)起來(lái),那么空間的節(jié)省就不會(huì)太多。
共享的Flyweight越多,存儲(chǔ)節(jié)約也就越多,節(jié)約量隨著共享狀態(tài)的增多而增大。當(dāng)對(duì)象使用大量的內(nèi)部及外部狀態(tài),并且外部狀態(tài)是計(jì)算出來(lái)的而非存儲(chǔ)的時(shí)候,節(jié)約量將達(dá)到最大。所以,可以使用兩種方法來(lái)節(jié)約存儲(chǔ):用共享減少內(nèi)部狀態(tài)的消耗;用計(jì)算時(shí)間換取對(duì)外部狀態(tài)的存儲(chǔ)。
同時(shí),在實(shí)現(xiàn)的時(shí)候,一定要控制好外部狀態(tài)與共享對(duì)象的對(duì)應(yīng)關(guān)系,比如我在代碼實(shí)現(xiàn)部分,在CCheseboard類中使用了一個(gè)map進(jìn)行彼此之間的映射,這個(gè)映射在實(shí)際開發(fā)中需要考慮的。
好了,享元模式就總結(jié)到這里了。希望大家和我分享你對(duì)設(shè)計(jì)模式的理解。我堅(jiān)信:分享使我們更進(jìn)步。
PS:至于騰訊那幫伙計(jì)到底是如何實(shí)現(xiàn)QQ游戲大廳的,我也不知道,這里也完全是猜測(cè)的,請(qǐng)不要以此為基準(zhǔn)。
相關(guān)文章
C++中關(guān)于std::queue?中遇到釋放內(nèi)存錯(cuò)誤的問題
這篇文章主要介紹了std::queue中遇到釋放內(nèi)存錯(cuò)誤的問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07C++面向?qū)ο髮?shí)現(xiàn)萬(wàn)年歷的示例代碼
本文將通過面向?qū)ο髮?shí)現(xiàn)一個(gè)簡(jiǎn)單的日歷(萬(wàn)年歷)效果,主要會(huì)有以下幾個(gè)模塊:模型、視圖、控制,感興趣的小伙伴可以動(dòng)手嘗試一下2022-06-06MATLAB Delaunay算法提取離散點(diǎn)邊界的方法
這篇文章主要為大家詳細(xì)介紹了MATLAB Delaunay算法提取離散點(diǎn)邊界的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12嵌入式C實(shí)戰(zhàn)項(xiàng)目開發(fā)技巧:對(duì)一個(gè)有規(guī)律的數(shù)組表進(jìn)行位移操作的方法
今天小編就為大家分享一篇關(guān)于嵌入式C實(shí)戰(zhàn)項(xiàng)目開發(fā)技巧:對(duì)一個(gè)有規(guī)律的數(shù)組表進(jìn)行位移操作的方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12C語(yǔ)言實(shí)現(xiàn)大數(shù)據(jù)文件的內(nèi)存映射機(jī)制
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)大數(shù)據(jù)文件的內(nèi)存映射機(jī)制的相關(guān)資料,需要的朋友可以參考下2017-01-01詳解如何利用C++實(shí)現(xiàn)Mystring類
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)MyString的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08基于C++詳解數(shù)據(jù)結(jié)構(gòu)(附帶例題)
數(shù)據(jù)結(jié)構(gòu)作為每一個(gè)IT人不可回避的問題,本文基于C++編寫,下面這篇文章主要給大家介紹了關(guān)于數(shù)據(jù)結(jié)構(gòu)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06