設(shè)計(jì)模式精讀 ~ 省略構(gòu)造細(xì)節(jié)的克隆技術(shù) ~ 原型

所屬文章系列:尋找塵封的銀彈:設(shè)計(jì)模式精讀


【一灸姊、從繁雜的代碼中尋找簡化之法】

【動(dòng)機(jī)】

程序員都知道設(shè)計(jì)模式是好東西,一開始都能動(dòng)力十足地去學(xué)習(xí)房匆。但是酌伊,時(shí)間久了才發(fā)現(xiàn):設(shè)計(jì)模式很難學(xué),《設(shè)計(jì)模式》相關(guān)書籍里的細(xì)節(jié)非常復(fù)雜卷员,學(xué)起來很吃力盈匾。即便學(xué)會(huì)了,用的地方也不多毕骡,因?yàn)轫?xiàng)目的時(shí)間壓力很大削饵。即便有機(jī)會(huì)用岩瘦,也會(huì)發(fā)現(xiàn)不知道何時(shí)該用哪種設(shè)計(jì)模式,這才是關(guān)鍵葵孤!因?yàn)檫@個(gè)問題一旦解決好了担钮,項(xiàng)目壓力再大,代碼也會(huì)寫得很快很漂亮尤仍,而且Bug少箫津。

本文要討論的“原型模式”,它的應(yīng)用場景就有點(diǎn)模糊宰啦,《設(shè)計(jì)模式》書中舉的“代碼示例”苏遥,把抽象工廠模式和原型模式放在一起講,導(dǎo)致我對這個(gè)模式的理解一度走偏赡模。后來田炭,經(jīng)過不斷的思考和實(shí)踐,終于弄明白了漓柑。

我們以Windows下的“畫圖”應(yīng)用程序?yàn)槔塘颍纯此拇a的痛點(diǎn)到底在哪里?當(dāng)我們使用原型模式之后辆布,再看看它會(huì)給我們帶來什么驚喜瞬矩?

先看一段未使用原型模式的代碼,當(dāng)然這些代碼并不是Windows的源碼:

當(dāng)用戶把一個(gè)圖形元素拖拽到畫布上時(shí)锋玲,會(huì)調(diào)用如下函數(shù):

void GraphicTool::UserCreateGraphicItem(int userSelected) {

????Graphic *newItem = NULL;


????switch (userSelected)

????case BUTTON: {

newItem = new Button(p1, p2, ...); // p1等參數(shù)需要從其他類中獲取

????????break;

????}

????case LABEL: {

????????newItem = new Label(p1, p2, ...);

????????break;

????}

????...

????default: {

????????return;

????}


????Canvas::InsertGraphicItem(newItem);

}

void Canvas::InsertGraphicItem(Graphic *newItem) {

????ItemList.Add(newItem);

????...

}

這些代碼看起來中規(guī)中矩景用,似乎沒有什么可改進(jìn)的余地。但是惭蹂,當(dāng)有一個(gè)新需求到來的時(shí)候伞插,我們新寫了一些代碼以支持新功能,代碼都運(yùn)行正常盾碗,卻總是感覺哪里不對媚污。

新需求是這樣的:用戶可以在菜單里選擇“拷貝到新圖片”,也就是說廷雅,把用戶正在畫的圖全盤拷貝到一個(gè)新圖片編輯窗口里杠步,代碼可以這樣寫:

void GraphicTool::UserClickClone() {

????Canvas *newCanvas = new Canvas(...);

????newCanvas->Brush = currentWindow->Canvas->Brush;

????newCanvas->Font = currentWindow->Canvas->Font;

????...


????for (Graphic *item = newCanvas->ItemList.first(); item < newCanvas->ItemList.end(); ++item) {

????????switch (item->Type())

????????case BUTTON: {

//需要寫一些獲取p1, p2等參數(shù)的代碼

????????????newItem = new Button(p1, p2, ...);

????????????break;

????????}

????????case LABEL: {

????????????newItem = new Label(p1, p2, ...);

????????????break;

????????}

????????...

????????default: {

????????????return;

????????}


????????item = newItem;

????}

currentWindow = new Window(); //切換到新窗口

????currentWindow->Canvas = newCanvas;

}

代碼算是可以工作了,但是總感覺不舒服榜轿,大概是因?yàn)镚raphicTool::UserClickClone的前邊只是為了克隆一個(gè)新對象,代碼卻寫了那么多朵锣,而且構(gòu)造過程的那些類名如Button谬盐、參數(shù)如p1,都需要了解得很清楚诚些,而且為了取到p1這些參數(shù)飞傀,需要從很多個(gè)類中去取皇型,費(fèi)盡了周折。最為頭疼的是砸烦,有時(shí)取到的參數(shù)并非想要的值弃鸦,這種情況測試起來就比較困難,總會(huì)有一些Bug直接出現(xiàn)在用戶面前幢痘。

有的人就會(huì)想:有沒有一個(gè)好方法唬格,能夠讓這段代碼看起來很簡單,而且不會(huì)出錯(cuò)颜说?再貪心一點(diǎn)购岗,代碼在面對未來的需求變化時(shí),能保持較小的改動(dòng)门粪?

【典型代碼】

這里就是原型模式一展身手的地方了喊积!

加入原型模式后,代碼成為了如下的樣子:

void GraphicTool::UserClickClone() {

Canvas *newCanvas = currentWindow->Canvas->Clone(); //原型模式

????currentWindow = new Window();

????currentWindow->Canvas = newCanvas;

}

不過需要Canvas支持Clone:

Canvas *Canvas::Clone() {

????Canvas *newCanvas = new Canvas(...);

????for (Graphic *item = ItemList.first(); item < ItemList.end(); ++item) {

????????newCanvas->ItemList.add(item->Clone());

????}

????...

????return newCanvas;

}

還需要Graphic的所有子類支持Clone:

Graphic *Button::Clone() {

//對每個(gè)成員變量賦值

}

Graphic *Label::Clone() {

//對每個(gè)成員變量賦值

}

【優(yōu)劣對比】

本質(zhì)上玄妈,使用原型模式的代碼乾吻,就是把GraphicTool::UserClickClone原有代碼中克隆Canvas的實(shí)現(xiàn)代碼,分散到了Canvas類拟蜻、Graphic各個(gè)子類中去了绎签。粗看起來總代碼量沒有多少變化,而實(shí)際上是減少了瞭郑!

減少的那部分代碼辜御,就是靠Canvas::Clone做到的。它把原來需要知道Graphic各個(gè)子類的類名如Button的代碼屈张,用多態(tài)的方式給消除掉了擒权。并且原來調(diào)用這些子類構(gòu)造函數(shù)時(shí)獲取參數(shù)的過程,也被轉(zhuǎn)移到各個(gè)子類阁谆,而且直接從子類的成員變量中獲取即可碳抄,不需求到其他類中尋找,非常方便而且不易出錯(cuò)场绿。

使用原型模式的回報(bào)還遠(yuǎn)不止此:

1.代碼量小剖效,見前文。

2.在獲取構(gòu)造Graphic各個(gè)子類時(shí)焰盗,減少了獲取構(gòu)造參數(shù)的繁雜過程璧尸,也避免了第一次構(gòu)造和克隆的代碼重復(fù)。

3.不需要知道Graphic各個(gè)子類的類名熬拒,這樣一來爷光,擴(kuò)展性就出來了,一旦再有Graphic的新子類加入澎粟,GraphicTool::UserClickClone和Canvas::Clone兩個(gè)函數(shù)都不需要修改任何一行代碼蛀序,只需要實(shí)現(xiàn)子類的Clone函數(shù)即可欢瞪。

4.GraphicTool::UserClickClone和Canvas::Clone兩個(gè)函數(shù)的思路非常清晰,代碼一看就懂徐裸。


【二遣鼓、模式核心】

【定義】

原型模式(Prototype):用一句話概括就是“省略構(gòu)造細(xì)節(jié)的克隆技術(shù)”。

展開來講重贺,它就是一種克隆復(fù)雜對象的解決方案:客戶代碼在克隆的時(shí)候骑祟,不需要知道克隆對象的細(xì)節(jié),包括對象所屬的類檬姥、構(gòu)造對象的參數(shù)等曾我。

類圖如下:

【適用場景】

在考慮使用原型模式之前,我們需要問自己一個(gè)問題:為什么要克隆健民,而不是使用已有的對象抒巢,或者直接構(gòu)造一個(gè)?

看下面使用已有對象的例子秉犹,兩個(gè)函數(shù)之間以共享的方式傳遞參數(shù):

void Client1::DoSomething() {

????Share *share = new Share();

????share->Value = 1;


????Client2 *client2 = new Client2();

????client2->DoSomething(share);


????if (share->Value) ...

}

void Client2::DoSomething(Share *share) {

????share->Value = share->Value == 0 ? 1 : share->Value;

}

在這個(gè)例子中蛉谜,根本不需要克隆。相反崇堵,如果克隆了對象型诚,反倒錯(cuò)了,這是因?yàn)閮蓚€(gè)函數(shù)之間需要共享對象鸳劳,而克隆對應(yīng)的是不共享狰贯。

那什么時(shí)候必須使用克隆,而達(dá)到不共享的目的呢赏廓?由外部需求的驅(qū)動(dòng)涵紊,例如前文的GraphicTool::UserClickClone,或者多線程的需要幔摸。我以標(biāo)準(zhǔn)化的形式來表達(dá)這種情況摸柄,參見下面的CloneRequirement::DoSomething:

void CloneRequirement::DoSomething() {

????CloneBase *newClone = client2->Clone();

... //把newClone傳遞給某個(gè)對象或某個(gè)線程

}

CloneBase *Client2::Clone() {

????CloneBase *newClone = new Client2(...);

????newClone->share = this->share->Clone();

????...

????return newClone;

}

【思維進(jìn)階(一):原型與值對象的區(qū)別】

有一個(gè)“值對象模式”與原型模式相關(guān)。

原型的本質(zhì)就是由客戶代碼來決定克隆一個(gè)對象既忆,而值對象的本質(zhì)是由模式內(nèi)部代碼決定每次構(gòu)造一個(gè)新對象驱负。

例如“拼接一個(gè)字符串”的表達(dá)式還可以寫成這樣:

????strBuffer.append("<").append(">");

它是用簡潔的形式來代替如下這種繁瑣的表達(dá)式:

????strBuffer.append("<");

????strBuffer.append(">");

這里用到的技術(shù)就是“值對象模式”,這個(gè)模式出現(xiàn)在《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書中患雇。

在append函數(shù)中跃脊,每調(diào)用一次都會(huì)生成一個(gè)新對象,這樣就避免了共享對象時(shí)苛吱,有時(shí)會(huì)發(fā)生錯(cuò)誤讀寫對象的問題匾乓。還有一個(gè)好處是讓表達(dá)式準(zhǔn)確、簡潔地表達(dá)程序員的意圖又谋,代價(jià)就是C++需要自己考慮內(nèi)存釋放的問題拼缝,而Java則沒有這個(gè)問題。

“值對象”的概念用一段文字不可能講得清楚彰亥,由于它和原型模式有一些相關(guān)咧七,本文只是粗略講解一下。有機(jī)會(huì)我會(huì)單開一篇文章探討它任斋。

【思維進(jìn)階(二):單一職責(zé)】

上一篇《設(shè)計(jì)模式精讀~單元測試的利器 ~ 抽象工廠模式》中講過一些關(guān)于“兩個(gè)維度的變化”以及“單一職責(zé)”的內(nèi)容继阻,這篇的原型模式也和這個(gè)有關(guān),只不過是具體的維度有所不同罷了废酷。

原型模式有兩個(gè)大維度瘟檩,每個(gè)大維度里各有兩個(gè)小維度:

1.克隆維度:誰負(fù)責(zé)克隆,傳遞給誰澈蟆。

2.構(gòu)造細(xì)節(jié)維度:如何確定被克隆的子類是什么墨辛,如何為新對象的每個(gè)成員變量賦值。

“兩個(gè)維度的變化”背后的原則是“單一職責(zé)”趴俘,也就是說睹簇,每個(gè)維度對于一個(gè)職責(zé)×壬粒“單一職責(zé)”的背后是人腦一次面對的概念越少越好太惠,容易理解和記憶,也容易發(fā)現(xiàn)代碼的漏洞疲憋。

如果人腦一次關(guān)注的維度過多凿渊,就自然會(huì)產(chǎn)生懈怠,導(dǎo)致很多代碼漏洞的發(fā)生缚柳。

我們來看多個(gè)維度會(huì)產(chǎn)生怎樣的變化埃脏,例如本節(jié)里剛剛提到的


“誰負(fù)責(zé)克隆”有三種情況,

“傳遞給誰”有四種情況喂击,

“如何確定被克隆的子類是什么”有十種情況剂癌,

“如何為新對象的每個(gè)成員變量賦值”有二十種情況。


那么把這四個(gè)維度都考慮進(jìn)來翰绊,就有3*4*10*20=2400種變化佩谷!人腦見到這種情況,一定會(huì)退避三舍监嗜,那么Bug自然就能找到生長的土壤谐檀。

另外,把維度分開或者單一職責(zé)還有一個(gè)好處裁奇,就是方便單元測試桐猬,因?yàn)槊總€(gè)用例只考慮一個(gè)變化,單獨(dú)測試時(shí)只有3+4+10+20=42個(gè)用例刽肠,反觀多個(gè)維度一起測試就是2400個(gè)用例溃肪!

【三免胃、細(xì)節(jié)、例外】

【擴(kuò)展】

像其他創(chuàng)建型模式一樣惫撰,原型模式可以通過自己實(shí)現(xiàn)的內(nèi)部注冊表機(jī)制羔沙,來實(shí)現(xiàn)子類原型的動(dòng)態(tài)載入。

【限制】

一厨钻、有些類不能修改扼雏,因?yàn)樗鼈兪且惶椎谌筋悗欤蛘呤瞧渌麍F(tuán)隊(duì)的代碼夯膀,例如Graphic子類或者子類引用的一些下游類诗充。那么只能在自己可控的范圍內(nèi)增加Clone函數(shù),在其他地方實(shí)現(xiàn)克隆過程但分散到子類中诱建,就像前文“未使用原型模式的代碼”那樣蝴蜓。

二、如果克隆對象之間涂佃,有循環(huán)引用的關(guān)系励翼,就很難實(shí)現(xiàn)克隆了。雖然循環(huán)引用不好辜荠,但實(shí)際的代碼總會(huì)有一些這種情況存在汽抚,而想改變循環(huán)引用的現(xiàn)狀又很困難,只好退而求其次了伯病。

【注意事項(xiàng)】

Prototype的子類都必須實(shí)現(xiàn)Clone造烁,這有時(shí)會(huì)很困難。例如午笛,當(dāng)子類已經(jīng)存在時(shí)惭蟋,克隆會(huì)迫使你立刻做出決定:是不是所有的指針都需要克隆一份兒?也就是深拷貝還是淺拷貝的問題药磺。

有時(shí)做這種決定是很困難的告组。不過在做這個(gè)決定的時(shí)候,有時(shí)會(huì)發(fā)現(xiàn)一些隱藏得很深的Bug癌佩。因?yàn)樵瓉淼拇a也不知道哪些該克隆哪些該共享木缝,而錯(cuò)誤地使用了編譯器默認(rèn)實(shí)現(xiàn)的淺拷貝方式(即共享方式),最常見的例子就是兩個(gè)對象共享一個(gè)字符串對象指針围辙。


作于2018-5-17


附:這次的代碼格式有點(diǎn)亂我碟,等我找到一個(gè)好的代碼格式再更新這篇文章,抱歉了姚建。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末矫俺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌厘托,老刑警劉巖友雳,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異催烘,居然都是意外死亡沥阱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進(jìn)店門伊群,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人策精,你說我怎么就攤上這事舰始。” “怎么了咽袜?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵丸卷,是天一觀的道長。 經(jīng)常有香客問我询刹,道長谜嫉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任凹联,我火速辦了婚禮沐兰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔽挠。我一直安慰自己住闯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布澳淑。 她就那樣靜靜地躺著比原,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杠巡。 梳的紋絲不亂的頭發(fā)上量窘,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天,我揣著相機(jī)與錄音氢拥,去河邊找鬼蚌铜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛兄一,可吹牛的內(nèi)容都是我干的厘线。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼出革,長吁一口氣:“原來是場噩夢啊……” “哼造壮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤耳璧,失蹤者是張志新(化名)和其女友劉穎成箫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旨枯,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹬昌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攀隔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皂贩。...
    茶點(diǎn)故事閱讀 40,021評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖昆汹,靈堂內(nèi)的尸體忽然破棺而出明刷,到底是詐尸還是另有隱情,我是刑警寧澤满粗,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布辈末,位于F島的核電站,受9級(jí)特大地震影響映皆,放射性物質(zhì)發(fā)生泄漏挤聘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一捅彻、第九天 我趴在偏房一處隱蔽的房頂上張望组去。 院中可真熱鬧,春花似錦沟饥、人聲如沸添怔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽广料。三九已至,卻和暖如春幼驶,著一層夾襖步出監(jiān)牢的瞬間艾杏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工盅藻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留购桑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓氏淑,卻偏偏與公主長得像勃蜘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子假残,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評論 2 355

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