第13章 結構型模式—享元模式
1. 享元模式(Flyweight Pattern)的定義
(1)運用共享技術高效地支援大量細粒度的物件
①物件內部狀態:資料不變且重複出現,這部分不會隨環境變化而改變,是可以共享的。
②物件外部狀態:資料是變化的,會隨環境變化而改變,是不可以共享的。
③所謂的享元,就是把內部狀態的資料分離出來共享,通過共享享元物件,可以減少對記憶體的佔用。把外部狀態分離出來,放到外部,讓應用程式在使用的時候進行維護,並在需要的時候傳遞給享元物件使用。
④享元模式真正快取和共享的是享元的內部狀態,而外部狀態是不被快取共享的。同時內部狀態和外部狀態是獨立的,外部狀態的變化不會影響到內部狀態。
(2)享元模式的結構和說明
①Flyweight:介面或抽象類,通過這個介面Flyweight可以接受並作用於外部狀態。通過這個介面傳入外部的狀態,在享元物件的方法處理中可能會使用這些外部的資料。
②ConcreteFlyweight:具體的享元實現物件,必須是可共享的,為內部狀態提供成員變數進行儲存。(真正要被共享的物件)
③UnsharedConcreteFlyweight:非共享的享元實現物件,並不是所有的Flyweight實現物件都需要共享。非共享的享元實現物件通常是對共享享元物件的組合物件,這種物件雖然不需要加入到享元工廠中去共享,但也繼承自Flyweight,其好處是可以統一介面。注意,本例中的UnsharedConcreteFlyweight是從Flyweight繼承而來
④FlyweightFactory:為控制內部狀態的共享,並且讓外部能簡單地使用共享資料,提供一個工廠來管理享元,把它稱為享元工廠。其作用主要用來建立並管理共享的享元物件,享元池一般設計成鍵值對。這裡也對外提供訪問共享享元的介面。
⑤Client:享元客戶端,主要的工作是維持一個對Flyweight的引用,計算或儲存享元物件的外部狀態。當然這裡可以訪問共享和不共享的Flyweight物件。
(3)思考享元模式
①享元模式的本質:分離與共享。享元模式的關鍵在於分離變與不變,把不變部分作為享元物件的內部狀態
②享元模式的變與不變:為什麼在一個地方要預留介面,一個常見的原因是這裡存在變化,可能在今後需要擴充套件或改變己有的實現,而預留介面作為“可插入性的保證”
③享元物件:又有共享(ConcreteFlyweight)與不共享(UnsharedConcreteFlyweight)之分。不共享一般出現在和組合模式合用的情況,通常共享是葉子物件,一般不共享的部分是由共享部分組合而成,由於所有細粒度的葉子物件己經快取了,那麼快取組合物件就沒有什麼意義。(如許可權管理中某安全實體的“檢視”和“修改”是可共享的,如果將“檢視”和“修改”組合成“操作”許可權的話,則“操作”許可權是不用快取(共享)的,因為己經在細粒度上進行了快取)。(見後面的例子)
【程式設計實驗】圍棋軟體設計
①內部狀態:每個圍棋那麼多的棋子,可設定為享元物件,有如下屬性顏色、大小、形狀。
②外部狀態:棋子在棋盤中的位置。這是不可共享的。
//結構型模式:享元模式
//場景:圍棋軟體設計。
//內部狀態——圍棋棋子數量多,但分只為兩類:白棋和黑棋。
// 有顏色、大小、形態等屬性,是可共享的物件。
//外部狀態——棋子在棋盤中的位置。
#include <iostream>
#include <string>
#include <map>
using namespace std;
//************************************享元類**************************
class Coordinate; //前向宣告
//享元抽象類
class ChessFlyweight
{
public:
virtual string& getColor() = 0;
virtual void setColor(string color) = 0;
//顯示棋子在棋盤中的位置
//可以通過這個介面,將外部狀態傳入享元物件中
virtual void display(Coordinate& c) = 0;
};
//非享元物件:UnsharedConcreteFlyweight(外部狀態)
class Coordinate
{
int x,y;
public:
Coordinate(int x, int y){this->x = x;this->y = y;}
int getX(){return x;}
void setX(int x){this->x = x;}
int getY(){return y;}
void setY(int x){this->y = y;}
};
//享元物件:ConcreteFlyweight(內部狀態)
class ConcreteChess : public ChessFlyweight
{
private:
string color;
public:
ConcreteChess(string color){this->color = color;}
string& getColor() {return color;}
void setColor(string color){this->color = color;}
void display(Coordinate& c)
{
cout << "Chess's Color: " << color << endl;
cout << "Position: x = " << c.getX() << " y = " << c.getY() << endl;
}
};
//************************************享元工廠類**************************
class ChessFlyweightFactory
{
private:
static map<string,ChessFlyweight*> chessMap;
public:
static ChessFlyweight* getChess(string color)
{
ChessFlyweight* ret = chessMap[color];
if(ret == NULL)
{
ret = new ConcreteChess(color);
chessMap[color] = ret;
}
return ret;
}
static void clear()
{
map<string, ChessFlyweight*>::iterator iter = chessMap.begin();
while(iter != chessMap.end())
{
delete iter->second;
cout <<iter->second << endl;
++iter;
}
chessMap.clear();
}
};
map<string,ChessFlyweight*> ChessFlyweightFactory::chessMap;
int main()
{
ChessFlyweight* chess1 = ChessFlyweightFactory::getChess("black");
ChessFlyweight* chess2 = ChessFlyweightFactory::getChess("black");
ChessFlyweight* chess3 = ChessFlyweightFactory::getChess("white");
cout << "chess1 = " << chess1 << endl;
cout << "chess2 = " << chess2 << endl;
cout << "chess3 = " << chess3 << endl;
//增加外部狀態的處理
cout << "extrinsic state: " << endl;
Coordinate c1(10, 10);
Coordinate c2(20, 20);
Coordinate c3(30, 30);
chess1->display(c1);
chess2->display(c2);
chess3->display(c3);
//刪除享元池中的物件
ChessFlyweightFactory::clear();
return 0;
}
2. 享元模式的物件管理
(1)實用引用計數的基本思路
在享元工廠中定義另外一個map,它的key值與快取物件的key是一樣的,而value就是被引用的次數,這樣當外部每次獲取該享元的時候,就把對應的引用計數加1,然後再記錄回去。
(2)實現垃圾回收的基本思路
①確定垃圾:定義一個快取物件的配置物件,在這個物件中描述了快取的開始時間和最長不被使用時間,則當前的時間-快取開始時間≥最長不被使用使間是表示為垃圾。當然每次物件被使用時,就把那個快取開始的時間更新為使用時的當前時間。
②回收時機:判斷出是垃圾的時候就可以回收了。誰來判斷垃圾?一般定義一個內部執行緒,這個執行緒在享元工廠被建立時啟動,每隔一定的時間來迴圈快取中所有物件的快取配置,看是否是垃圾,如果是就可以啟動回收了。
(3)怎麼回收:直接從快取的Map物件刪除相應的物件。
3. 享元模式的優缺點
(1)優點:減少物件數量,節省記憶體空間
(2)缺點:維護共享物件,需要額外開銷。如需要額外的執行緒來維護垃圾回收。
4. 使用場景
(1)系統中存在大量的相似物件
(2)細粒度的物件都具有較接近的外部狀態,而且內部狀態與環境無關,也就是說對明沒有特定的身份。
(3)需要緩衝池的場景
(4)如果不考慮物件的外部狀態,可以用相對較少的共享物件取代很多組合物件,可以使用享元模式來共享物件,然後組合物件來使用這些共享物件。
【程式設計實驗】許可權管理系統
//結構型模式:享元模式
//場景:許可權管理。
//幾個概念
//1.安全實體:如某個資料表
//2.許可權:指很對安全實體進行的操作,如檢視、修改、刪除等。
//幾個問題
//1.因安全實體的許可權可能被成千上萬的人共享。如“人員列表”的查詢許可權
// 為了減少建立物件,可以將“人員列表”的“查詢”許可權作為一個享元物件,放入享元工廠
//2.為了增加實用性,這裡採用享元模式+組合模式的方式實現了多層次的許可權管理。
// 組合:將安全實體的許可權進行組合,如“檢視”+“修改” = “操作”許可權,由於“操作”是組
// 合的許可權,所以無須在享元工廠中快取,即這個組合物件是個UnsharedConcreteFlyweight
// 物件。
//3. 享元工廠的垃圾回收:建立一個執行緒,專門負責過期的垃圾回收
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <sstream>
#include <ctime>
#include <windows.h>
#include <process.h>
using namespace std;
//////////////////////////////////////////////////////////////////////////
// 字串分割
//
// -------------------------------------------------------------------------
// 函式 : Split
// 功能 : 分割STL標準字串
// 返回值 : void
// 引數 : Container<std::basic_string<CharT> >& v 存放分割結果
// 引數 : const std::basic_string<CharT>& s 待分割字串
// 引數 : const std::basic_string<CharT>& c 分割字串
// -------------------------------------------------------------------------
template<typename CharT, template<typename S, typename Q = std::allocator<S> > class Container>
void Split(Container<std::basic_string<CharT> >& v, const std::basic_string<CharT>& s, const std::basic_string<CharT>& c);
template<template<typename S, typename Q = std::allocator<S> > class Container>
void Split(Container<std::basic_string<char> >& v, const std::basic_string<char>& s, const std::basic_string<char>& c)
{
if (0 == c.length())
return;
std::basic_string<char>::size_type pos1 = 0;
std::basic_string<char>::size_type pos2 = 0;
pos1 = 0;
pos2 = s.find(c);
while (std::basic_string<char>::npos != pos2)
{
v.push_back(s.substr(pos1, pos2 - pos1));
pos1 = pos2 + c.size();
pos2 = s.find(c, pos1);
}
if (pos1 != s.length())
{
v.push_back(s.substr(pos1));
}
}
//*******************************************輔助類******************************
//測試資料(在記憶體中模擬資料庫中的值)
class TestDB
{
private:
TestDB(){}
public:
//用來存放單獨授權資料的值
static vector<string> vectorDB;
//用來存放組合授權資料的值
//其中的key為組合資料的id,value為該組合包含的多條授權資料的值
static map<string, vector<string> > mapDB;
};
//單獨授權資料
static vector<string>::value_type init_value[] =
{
vector<string>::value_type("ZhangSan,PersonTable,Query,1"),
vector<string>::value_type("LiSi,PersonTable,Query,1"),
vector<string>::value_type("LiSi,OperateSalaryTable,,2"),
vector<string>::value_type("ZhangSan0,PersonTable,Query,1"),
vector<string>::value_type("ZhangSan1,PersonTable,Query,1"),
vector<string>::value_type("ZhangSan2,PersonTable,Query,1"),
};
vector<string> TestDB::vectorDB(init_value, init_value + 6);
//組合授權資料
static vector<string>::value_type initValue[] =
{
vector<string>::value_type("SalaryTable,Query"),
vector<string>::value_type("SalaryTable,Modify")
};
static vector<string> compositePermit(initValue, initValue + 2);
static map<string, vector<string> >::value_type initMap_value[] =
{
map<string, vector<string> >::value_type("OperateSalaryTable", compositePermit)
};
map<string, vector<string> > TestDB::mapDB(initMap_value, initMap_value + 1);
//**********************************享元類************************
//享元介面:描述授權資料的介面
class Flyweight
{
public:
virtual bool match(string securityEntity, string permit) = 0;
//享元模式與組合模式的結合。為Flyweight新增子Flyweight物件
virtual void add(Flyweight* f) = 0;
};
//具體享元物件(封裝授權資料中重複出現的部分)
//由於add是針對組合物件的,而這個可共享的是葉子物件,所以這裡丟擲
//異常就可以了。
//享元工廠裡存放的是這個物件,如“薪資資料表的檢視許可權”。因為某一許可權
//可能被分配給成上千萬個人,所以需要快取。即享元
class AuthorizationFlyweight : public Flyweight
{
private:
//內部狀態
string securityEntity;//安全實體
string permit; //許可權
public:
//建構函式,傳入狀態資料,包含安全實體和許可權的資料,用“,”分隔
AuthorizationFlyweight(string state)
{
vector<string> v;
Split(v, state, ",");
securityEntity = v[0];
permit = v[1];
}
string& getSecurityEntity(){ return securityEntity; }
string& getPermit(){ return permit; }
bool match(string securityEntity, string permit)
{
bool ret = (this->securityEntity == securityEntity &&
this->permit == permit);
return ret;
}
void add(Flyweight* f)
{
//葉子物件,什麼都不做!這裡也可以丟擲異常!
}
};
//不需要共享的享元物件的實現,也是組合模式中的組合物件
class UnsharedConcreteFlyweight : public Flyweight
{
private:
//記錄每個組合物件所包含的子元件
vector<Flyweight*> flyweights;
public:
void add(Flyweight* f)
{
flyweights.push_back(f);
}
bool match(string securityEntity, string permit)
{
bool ret = false;
vector<Flyweight*>::iterator iter = flyweights.begin();
while (iter != flyweights.end())
{
if ((*iter)->match(securityEntity, permit))
{
ret = true;
break;
}
++iter;
}
return ret;
}
};
//**************************************垃圾回收************************
//描述享元物件快取的配置物件
class CacheConfModel
{
private:
//被引用次數
int refCount;
//快取開始計時的開始時間
long beginTime;
//快取物件存放的持續時間,其實是最長不被使用時間
double durableTime;
//快取物件需要被永久儲存,也就是不需要從快取中刪除
bool forever;
public:
CacheConfModel() :refCount(0), beginTime(0), durableTime(0), forever(false){}
int getRefCount(){ return refCount; }
void setRefCount(int refCount){ this->refCount = refCount; }
int addRefCount(){ return ++refCount; }
int decRefCount(){ return --refCount; }
bool isForever(){ return forever; }
void setForever(bool forever){ this->forever = forever; }
long getBeginTime(){ return beginTime; }
void setBeginTime(long beginTime){ this->beginTime = beginTime; }
double getDurableTime(){ return durableTime; }
void setDurableTime(double durableTime){ this->durableTime = durableTime; }
};
//******************************************************************
//享元工廠,通常實現為單例
//加入實現垃圾回收和引用計數的功能
class FlyweightFactory
{
private:
//建構函式設為私有
FlyweightFactory()
{
//啟動清除快取值的執行緒
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, clearCache, NULL, 0, NULL);
};
//快取多個Flyweight物件
static map<string, Flyweight*> fsMap;
//用來快取被共享物件的快取配置,key和上面fsMap一致
static map<string, CacheConfModel> cacheConfMap;
//預設儲存6秒鐘,主要為了測試方便,這個時間可以根據實際來設定
static const long DURABLE_TIME = 6L;
//刪除key對應的享元物件,連帶清除對應的快取配置
void removeFlyweight(string key) //執行緒不安全,實際應用中要加同步
{
fsMap.erase(key);
cacheConfMap.erase(key);
}
//維護清除快取的執行緒函式
static unsigned int __stdcall clearCache(PVOID pvParam)
{
while (true)
{
map<string, CacheConfModel>::iterator iter = cacheConfMap.begin();
vector<string> tmpKey;
CacheConfModel ccm;
while (iter != cacheConfMap.end())
{
ccm = iter->second;
//比較是否需要清除
if (time(NULL) - ccm.getBeginTime() >=
ccm.getDurableTime())
{
//可以清除,先記錄下來
tmpKey.push_back(iter->first);
}
++iter;
}
//真正清除
vector<string>::iterator iterKey = tmpKey.begin();
while (iterKey != tmpKey.end())
{
(FlyweightFactory::getInstance())->removeFlyweight(*iterKey);
++iterKey;
}
//顯示
cout << "now thread=" << fsMap.size() << " [";
map<string, Flyweight*>::iterator fsIter = fsMap.begin();
while (fsIter != fsMap.end())
{
cout << fsIter->first << ";";
++fsIter;
}
cout << "]" << endl;
//休息1秒鐘,再重新判斷
Sleep(1000);
}
return 0;
}
public:
static FlyweightFactory* getInstance(){
static FlyweightFactory factory;
return &factory;
}
//快取多個Flyweight物件(執行緒不安全,實際應用中要加同步)
Flyweight* getFlyweight(string key)
{
Flyweight* f = fsMap[key];
if (f == NULL)
{
f = new AuthorizationFlyweight(key);
fsMap[key] = f;
//同時設定快取配置資料和引用計數
CacheConfModel cm;
cm.setBeginTime((long)time(NULL));
cm.setDurableTime(DURABLE_TIME);
cm.setForever(false);
cm.addRefCount();
cacheConfMap[key] = cm;
} else
{
CacheConfModel cm = cacheConfMap[key];
cm.setBeginTime((long)time(NULL));
cm.addRefCount();
cacheConfMap[key] = cm;
}
return f;
}
//獲取某個享元被使用的次數(執行緒不安全,沒有同步!)
int getUseTimes(string key)
{
CacheConfModel ccm = cacheConfMap[key];
return ccm.getRefCount();
}
};
map<string, Flyweight*> FlyweightFactory::fsMap;
map<string, CacheConfModel> FlyweightFactory::cacheConfMap;
//安全管理,實現成單例
class SecurityMgr
{
private:
SecurityMgr(){}
public:
//餓漢式
static SecurityMgr* getInstance()
{
static SecurityMgr instance;
return &instance;
}
//從資料庫中獲取某人所擁有的許可權
vector<Flyweight*> queryByUser(string user)
{
vector<Flyweight*> ret;
vector<string> vc;
vector<string>::iterator iter = TestDB::vectorDB.begin();
while (iter != TestDB::vectorDB.end())
{
vc.clear();
Split(vc, *iter, ",");
if (vc.size() <4) break;
if (vc[0] == user)
{
Flyweight* fm = NULL;
if (vc[3] == "2"){
//表示組合
fm = new UnsharedConcreteFlyweight();
vector<string> tempSs = TestDB::mapDB[vc[1]];
vector<string>::iterator iter = tempSs.begin();
while (iter != tempSs.end())
{
Flyweight* tempFm = (FlyweightFactory::getInstance())->getFlyweight(*iter);
//把這個物件加入到組合物件中
fm->add(tempFm);
++iter;
}
} else
{
fm = (FlyweightFactory::getInstance())->getFlyweight(vc[1] + "," + vc[2]);
}
ret.push_back(fm);
}
++iter;
}
return ret;
}
//判斷某使用者對某個安全實體是否擁有某種許可權
bool hasPermit(string user, string securityEntity, string permit)
{
bool ret = false;
vector<Flyweight*> vfw = queryByUser(user);
//cout << "Now testing: " << securityEntity << "\'s " << permit << " permission, map.size = "
//<< maps.size() << endl;
if (vfw.size() == 0)
{
cout << user << ": no permit to operate " << securityEntity << endl;
return ret;
}
vector<Flyweight*>::iterator iter = vfw.begin();
while (iter != vfw.end())
{
cout << "am == " << (*iter) << endl;
if ((*iter)->match(securityEntity, permit))
{
ret = true;
break;
}
++iter;
}
return ret;
}
};
int main()
{
//客戶端呼叫
//需要先登入,然後再判斷是否有許可權
SecurityMgr* mgr = SecurityMgr::getInstance();
bool f1 = mgr->hasPermit("ZhangSan", "PersonTable", "Query");
bool f2 = mgr->hasPermit("LiSi", "SalaryTable", "Query");
bool f3 = mgr->hasPermit("LiSi", "SalaryTable", "Modify");
cout << "f1 = " << f1 << endl;
cout << "f2 = " << f2 << endl;
cout << "f3 = " << f3 << endl;
ostringstream oss;
for (int i = 0; i < 3; i++){
oss.str("");
oss << i;
string name = "ZhangSan" + oss.str();
mgr->hasPermit(name, "SalaryTable", "Query");
}
//檢視引用次數,不是指測試使用的次數,指的是SecurityMgr裡的
//queryByUser方法通過享元工廠去獲以享元物件的次數
cout << "Salary, Query reference count: " << (FlyweightFactory::getInstance())->getUseTimes("SalaryTable,Query") << endl;
cout << "Salary, Modify reference count: " << (FlyweightFactory::getInstance())->getUseTimes("SalaryTable,Modify") << endl;
cout << "Person, Query reference count: " << (FlyweightFactory::getInstance())->getUseTimes("PersonTable,Query") << endl;
system("pause");
return 0;
}
5. 相關模式
(1)享元模式與組合模式
在享元模式中,存在不需要共享的享元實現(UnsharedConcreteFlyweight),這些不需要共享的享元通常是對共享的享元物件的組合物件。也就是說,享元模式通常會和組合模式使用,來實現更復雜的物件層次結構。
(2)享元模式與狀態模式
可以使用享元模式來共享狀態模式中的狀態物件,通常在狀態模式中,會存在數量很大的、細粒度的狀態物件,而且它們基本上是可以重複使用的,都是用來處理某一個固定的狀態的,它們需要的資料通常是由外部傳入的,也就是變化的部分分離出去了,所以可以用享元模式來實現這些狀態物件。
(3)享元模式與策略模式
可以使用享元模式來實現策略模式中的策略物件。和狀態模式一樣,在策略模式中也存在大量細粒度的策略物件,它們需要的資料同樣從上下文傳入,所以也可以使用享元模式來快取在這些策略物件。