原型模式探討

概念

原型模式屬于設(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模式的類圖是這樣的:

    • prototype_pattern_class_graph
      prototype_pattern_class_graph

: 通過繼承機(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.

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饵婆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子戏售,更是在濱河造成了極大的恐慌侨核,老刑警劉巖草穆,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異搓译,居然都是意外死亡悲柱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門些己,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豌鸡,“玉大人,你說我怎么就攤上這事段标⊙墓冢” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵逼庞,是天一觀的道長功偿。 經(jīng)常有香客問我,道長往堡,這世上最難降的妖魔是什么械荷? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮虑灰,結(jié)果婚禮上吨瞎,老公的妹妹穿的比我還像新娘。我一直安慰自己穆咐,他們只是感情好颤诀,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著对湃,像睡著了一般崖叫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拍柒,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天心傀,我揣著相機(jī)與錄音,去河邊找鬼拆讯。 笑死脂男,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的种呐。 我是一名探鬼主播宰翅,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爽室!你這毒婦竟也來了汁讼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嘿架,沒想到半個月后卜录,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡眶明,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年艰毒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搜囱。...
    茶點(diǎn)故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡丑瞧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜀肘,到底是詐尸還是另有隱情绊汹,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布扮宠,位于F島的核電站西乖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏坛增。R本人自食惡果不足惜获雕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望收捣。 院中可真熱鬧届案,春花似錦、人聲如沸罢艾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咐蚯。三九已至童漩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間春锋,已是汗流浹背矫膨。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留看疙,地道東北人豆拨。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像能庆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子脚线,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容