概念
原型模式屬于設(shè)計模式的一種, 更準(zhǔn)確的說, 是一種創(chuàng)建型模式, 根據(jù)wikipedia的介紹:
其特點(diǎn)在于通過「復(fù)制」一個已經(jīng)存在的實例來返回新的實例,而不是新建實例盼产。被復(fù)制的實例就是我們所稱的「原型」
我更習(xí)慣稱原型模式為clone
模式, 因為原型對象往往都有一個clone()
成員函數(shù), 用于復(fù)制新的對象(克隆對象).
class Scorer {
public:
virtual Scorer* clone() = 0;
};
clone模式的類圖是這樣的:
注: 通過繼承機(jī)制, Client只需要和Prototype
打交道, 從而與原型對象的具體實現(xiàn)解耦合, 所以具體原型對象既可以是ConcretePrototype1, 也可以是ConcretePrototype2
好處
假設(shè)以下應(yīng)用場景, 從factory
中構(gòu)造對象需要復(fù)雜的操作(比如讀配置文件), 那創(chuàng)建大量的對象就非常低效, 此時, 原型模式就派上用場, factory
只負(fù)責(zé)創(chuàng)建第一個對象(原型對象), 后續(xù)生成的對象, 只要從原型對象clone()
就夠了, 運(yùn)行時直接拷貝往往代價很小. 這就是它的最大優(yōu)點(diǎn):
Prototype pattern refers to creating duplicate object while keeping performance in mind.
wiki 介紹了一個有趣的例子, 闡明了上述觀點(diǎn)
clone模式帶來的另一個好處是客戶端(client)避免接觸到原型對象的具體實現(xiàn). 比如在搜索引擎中, 排序Scorer的實現(xiàn)和業(yè)務(wù)相關(guān), 具體細(xì)節(jié)復(fù)雜而難懂, 讓引擎了解這些細(xì)節(jié), 只會增加系統(tǒng)復(fù)雜度, 正確的做法, 引擎需要調(diào)用Scorer的clone()
接口為每個請求生成一個新的Scorer.
avoid subclasses of an object creator in the client application, like the abstract factory pattern does.
我認(rèn)為它還有第三個好處, 特別適用于服務(wù)器端的請求處理, 比如搜索引擎, 后端請求處理一般是多線程的, 此時為每個請求clone一個對象(比如Scorer對象), 可以實現(xiàn)線程間數(shù)據(jù)的隔離, 相對比全局共享的singleton對象, 那真是清爽很多.
一個Scorer的例子
程序代碼:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
class IScorer {
public:
virtual ~IScorer() {};
public:
virtual int init() = 0;
virtual int processQuery(int query_info) = 0;
virtual int doScore(int doc_info) = 0;
public:
virtual IScorer *clone() = 0;
virtual int destroy() = 0;
};
class DemoScorer: public IScorer {
public:
virtual ~DemoScorer() {
// delete pGlobalInfo;
// pGlobalInfo = NULL;
};
public:
virtual int init() {
pGlobalInfo = new int();
*pGlobalInfo = rand();
}
virtual int processQuery(int query_info) {
fprintf(stdout, "process query[%d] in global[%d]\n", query_info, *pGlobalInfo);
_queryInfo = query_info;
}
virtual int doScore(int doc_info) {
fprintf(stdout, "\tscore for %d in query [%d], global [%d]\n", doc_info, _queryInfo, *pGlobalInfo);
}
public:
virtual IScorer *clone() {
return new DemoScorer(*this);
}
virtual int destroy() {
delete this;
}
private:
int* pGlobalInfo;
int _queryInfo;
};
class EngineRunner
{
public:
EngineRunner() {
_pScorer = new DemoScorer;
_pScorer->init();
};
~EngineRunner() {
delete _pScorer;
}
void start() {
for (int i = 0; i < 3; i++)
{
pthread_create(_threadIds+i, NULL, EngineRunner::threadEntry, this);
}
for (int i = 0; i < 3; i++)
{
pthread_join(_threadIds[i], NULL);
}
}
int threadFun() {
int query_info = rand();
IScorer *pScorer = _pScorer->clone();
pScorer->processQuery(query_info);
int docNum = 4;
for (int i=0; i<docNum; ++i) {
pScorer->doScore(i);
}
pScorer->destroy();
}
public:
static void * threadEntry(void * arg) {
EngineRunner* pRunner = (EngineRunner*)arg;
pRunner->threadFun();
}
private:
IScorer * _pScorer;
pthread_t _threadIds[3];
};
int main(void)
{
EngineRunner runner;
runner.start();
return 0;
}
上面的程序模擬了搜索引擎(EngineRunner)調(diào)用了Scorer的過程. 著重闡明如何使用clone模式實現(xiàn), 其他的細(xì)節(jié)都省略了.
搜索引擎(EngineRunner)起了3個工作線程, 分別處理用戶請求, 當(dāng)請求(query_info)到來, 引擎clone()
一個Scorer處理它, 處理完了之后, 調(diào)用destroy()
銷毀它
int query_info = rand();
IScorer *pScorer = _pScorer->clone();
pScorer->processQuery(query_info);
int docNum = 4;
for (int i=0; i<docNum; ++i) {
pScorer->doScore(i);
}
pScorer->destroy();
上述程序存在一個漏洞, 使用valgrind能輕易發(fā)現(xiàn)有內(nèi)存泄露的情況:
valgrind --leak-check=full ./main
==29000== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29000== at 0x4A06DC7: operator new(unsigned long) (vg_replace_malloc.c:261)
==29000== by 0x400C5C: DemoScorer::init() (in /home/jiye/blog/clone_pattern/main)
==29000== by 0x400A9F: EngineRunner::EngineRunner() (in /home/jiye/blog/clone_pattern/main)
==29000== by 0x400919: main (in /home/jiye/blog/clone_pattern/main)
那我們就fix它, 把析構(gòu)函數(shù)的注釋打開即可:
virtual ~DemoScorer() {
// delete pGlobalInfo;
// pGlobalInfo = NULL;
};
但是, 迅速我就發(fā)現(xiàn)了另一個問題, double free:
*** glibc detected *** ./main_v2: double free or corruption (fasttop): 0x00000000073cd030 ***
======= Backtrace: =========
/lib64/libc.so.6[0x32f18722ef]
/lib64/libc.so.6(cfree+0x4b)[0x32f187273b]
./main_v2(__gxx_personality_v0+0x399)[0x400b81]
./main_v2(__gxx_personality_v0+0x201)[0x4009e9]
./main_v2[0x400d44]
./main_v2[0x400d63]
深入分析, 發(fā)現(xiàn)全局資源(pGlobalInfo
)在clone()
中只是簡單的淺拷貝, 在第一個克隆對象(DemoScorer
)在析構(gòu)函數(shù)中刪除它, 導(dǎo)致后續(xù)克隆對象獲得的全局對象處于未定義狀態(tài).
直接了當(dāng)?shù)慕鉀Q這個問題, 那就重新定義DemoScorer
, 區(qū)分開原型對象和克隆對象, 原型對象負(fù)責(zé)析構(gòu)全局資源.
class DemoScorer: public IScorer {
public:
DemoScorer() {
_isClone = false;
}
virtual ~DemoScorer() {
if (! _isClone) {
delete pGlobalInfo;
pGlobalInfo = NULL;
}
};
public:
virtual int init() {
pGlobalInfo = new int();
*pGlobalInfo = rand();
}
virtual int processQuery(int query_info) {
fprintf(stdout, "process query[%d] in global[%d]\n", query_info, *pGlobalInfo);
_queryInfo = query_info;
}
virtual int doScore(int doc_info) {
fprintf(stdout, "\tscore for %d in query [%d], global [%d]\n", doc_info, _queryInfo, *pGlobalInfo);
}
public:
virtual IScorer *clone() {
DemoScorer *pScorer = new DemoScorer(*this);
pScorer->setClone(true);
return pScorer;
}
virtual int destroy() {
delete this;
}
void setClone(bool yes) { _isClone = yes; }
private:
bool _isClone;
int* pGlobalInfo;
int _queryInfo;
};
這樣就不會有內(nèi)存泄露
valgrind --leak-check=full ./main_v3
==29473== All heap blocks were freed -- no leaks are possible
改進(jìn)
深入的看這個問題, 問題的癥結(jié)在于全局資源和query級別資源都需要 DemoScorer
管理和釋放, 兩個不同作用域的資源應(yīng)該分開才好, 于是我能想到的是把全局資源的管理放到ScorerFactory
中, 這樣分開管理, 問題就引刃而解.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
class IScorer {
public:
virtual ~IScorer() {};
public:
virtual int processQuery(int query_info) = 0;
virtual int doScore(int doc_info) = 0;
public:
virtual IScorer *clone() = 0;
virtual int destroy() = 0;
};
class DemoScorer: public IScorer {
public:
DemoScorer(int* pGlobalInfo) {
_pGlobalInfo = pGlobalInfo;
}
virtual ~DemoScorer() {
};
public:
virtual int processQuery(int query_info) {
fprintf(stdout, "process query[%d] in global[%d]\n", query_info, *_pGlobalInfo);
_queryInfo = query_info;
}
virtual int doScore(int doc_info) {
fprintf(stdout, "\tscore for %d in query [%d], global [%d]\n", doc_info, _queryInfo, *_pGlobalInfo);
}
public:
virtual IScorer *clone() {
DemoScorer *pScorer = new DemoScorer(*this);
return pScorer;
}
virtual int destroy() {
delete this;
}
private:
int* _pGlobalInfo;
int _queryInfo;
};
class ScorerFactory {
public:
ScorerFactory() { _pGlobalInfo = NULL; };
~ScorerFactory() {
if (_pGlobalInfo) {
delete _pGlobalInfo;
_pGlobalInfo = NULL;
}
};
public:
int init() {
_pGlobalInfo = new int();
*_pGlobalInfo = rand();
}
IScorer* createScorer() {
return new DemoScorer(_pGlobalInfo);
}
void destroy(IScorer* pScorer) {
delete pScorer;
}
private:
int * _pGlobalInfo;
};
class EngineRunner
{
public:
EngineRunner() {
factory.init();
_pScorer = factory.createScorer();
};
~EngineRunner() {
factory.destroy(_pScorer);
}
void start() {
for (int i = 0; i < 3; i++)
{
pthread_create(_threadIds+i, NULL, EngineRunner::threadEntry, this);
}
for (int i = 0; i < 3; i++)
{
pthread_join(_threadIds[i], NULL);
}
}
int threadFun() {
int query_info = rand();
IScorer *pScorer = _pScorer->clone();
pScorer->processQuery(query_info);
int docNum = 4;
for (int i=0; i<docNum; ++i) {
pScorer->doScore(i);
}
pScorer->destroy();
}
public:
static void * threadEntry(void * arg) {
EngineRunner* pRunner = (EngineRunner*)arg;
pRunner->threadFun();
}
private:
IScorer* _pScorer;
ScorerFactory factory;
pthread_t _threadIds[3];
};
int main(void)
{
EngineRunner runner;
runner.start();
return 0;
}
上述代碼修改了IScorer
的接口, 把init()
遷移到了ScorerFactory
類中, 這樣全局資源的申請和釋放就遷移到了 ScorerFactory
. IScorer
只需要管理query級別的資源就夠了, 看上去清爽很多.
但是, 看上去很美的東西, 未必實用, 假如你要編寫50個Scorer
, 現(xiàn)在你就需要編寫100個類了, 50個Scorer + 50個ScorerFactory, 蛋疼.
繼續(xù)改進(jìn)
軟件世界中, 不存在適用各種情況的完美設(shè)計, 只要設(shè)計滿足現(xiàn)狀和將來的需要,那就是OK的.回到這個問題本身, 要讓全局資源和query資源都通過IScorer
進(jìn)行管理, 同時能合理的處理原型對象和克隆對象. 只需要為全局資源找一個釋放的接口即可destruct()
; 為了預(yù)制匹配, 我把全局資源的獲取從init()
改成construct()
.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
class IScorer {
public:
virtual int construct() = 0;
virtual int destruct() = 0;
public:
virtual int processQuery(int query_info) = 0;
virtual int doScore(int doc_info) = 0;
public:
virtual IScorer *clone() = 0;
virtual int recycle(IScorer *) = 0;
virtual ~IScorer() {};
};
class DemoScorer: public IScorer {
public:
virtual int construct() {
pGlobalInfo = new int();
*pGlobalInfo = rand();
}
virtual int destruct() {
delete pGlobalInfo;
pGlobalInfo = NULL;
};
virtual int processQuery(int query_info) {
fprintf(stdout, "process query[%d] in global[%d]\n", query_info, *pGlobalInfo);
_queryInfo = query_info;
}
virtual int doScore(int doc_info) {
fprintf(stdout, "\tscore for %d in query [%d], global [%d]\n", doc_info, _queryInfo, *pGlobalInfo);
}
public:
virtual IScorer *clone() {
return new DemoScorer(*this);
}
virtual ~DemoScorer() {
};
virtual int recycle(IScorer* pScorer) {
delete pScorer;
}
private:
int* pGlobalInfo;
int _queryInfo;
};
class EngineRunner
{
public:
EngineRunner() {
_pScorer = new DemoScorer;
_pScorer->construct();
};
~EngineRunner() {
_pScorer->destruct();
delete _pScorer;
}
void start() {
for (int i = 0; i < 3; i++)
{
pthread_create(_threadIds+i, NULL, EngineRunner::threadEntry, this);
}
for (int i = 0; i < 3; i++)
{
pthread_join(_threadIds[i], NULL);
}
}
int threadFun() {
int query_info = rand();
IScorer *pScorer = _pScorer->clone();
pScorer->processQuery(query_info);
int docNum = 4;
for (int i=0; i<docNum; ++i) {
pScorer->doScore(i);
}
_pScorer->recycle(pScorer);
}
public:
static void * threadEntry(void * arg) {
EngineRunner* pRunner = (EngineRunner*)arg;
pRunner->threadFun();
}
private:
IScorer * _pScorer;
pthread_t _threadIds[3];
};
int main(void)
{
EngineRunner runner;
runner.start();
return 0;
}
注意IScorer
接口的定義, 全局資源的獲取和釋放通過以下接口:
public:
virtual int construct() = 0;
virtual int destruct() = 0;
query資源的獲取和釋放通過以下接口
public:
virtual IScorer *clone() = 0;
virtual recycle(IScorer*) = 0;
virtual ~IScorer() {};
通過接口的定義, 從接口層面規(guī)范了IScorer的行為, <<Effective C++>> 條款18說:
Make interfaces easy to use collectly and hard to use incorrectly
我認(rèn)為這個版本相對于第一個版本的最大優(yōu)勢在于, 把復(fù)雜工作轉(zhuǎn)移到引擎的調(diào)用上. 最大程度讓Scorer的實現(xiàn)者focus在最重要的事情上-業(yè)務(wù).
后續(xù)
我問過一些人, 他們也給出很好的處理建議, 比如:
-
DemoScorer
的全局資源通過shared_ptr
管理起來, 通過引用計數(shù)就能實現(xiàn)全局資源的有效管理. -
DemoScorer
全局資源直接作為靜態(tài)成員數(shù)據(jù)static
, 由系統(tǒng)負(fù)責(zé)釋放.
但是我覺得, 在這個特定的場景(需要編寫大量的Scorer)中, 上述做法都增加了實現(xiàn)Scorer的復(fù)雜度.當(dāng)然換個場景, 那就另當(dāng)別論了.
Scott Meyers 說:
... That's a simple reflection of the fact that there is no one ideal design for all software. The best design depends on what the system is expected to do, both now and in the future.