模式羅漢拳: Immutable模式與string類的實現(xiàn)
透明
梗概
禁止改變對象的狀態(tài),從而增加共享對象的堅固性、減少對象訪問的錯誤,同時還避免了在多線程共享時進行同步的需要。
實現(xiàn)方法:在對象構(gòu)造完成以后就完全禁止改變?nèi)魏螤顟B(tài)信息。如果需要改變狀態(tài),則生成一個狀態(tài)與原對象不同的新對象。
場景
假設(shè)你正在為一家游戲公司開發(fā)一個和外太空、宇宙飛船有關(guān)的游戲,當然你有必要用某種方式來表示一艘宇宙飛船(不管它是屬于地球人的還是屬于外星人的)所處的位置。很自然的,你決定編寫一個Position類。從一個Position對象應(yīng)該可以查詢到當前位置的x坐標和y坐標(我們的游戲比較簡單,二維地圖,呵呵),還應(yīng)該可以根據(jù)輸入的偏移量得到新的位置。很正確的設(shè)計,不是嗎?(見例1)
例1:Position的設(shè)計
class Position{
private:
int x, y; //簡單點,用整型數(shù)來表示坐標
public:
Position(int x, int y){ //ctor需要兩個參數(shù)。
this->x = x;
this->y = y;
}
int getX( ){ return x; }
int getY( ){ return y; }
void Offset(int offX, int offY){ //根據(jù)偏移量得到新的位置
x+=offX;
y+=offY;
}
}
但是,如果我們的Position需要在多線程環(huán)境下使用,它能保證線程安全嗎?答案是很明顯的No!如果兩條線程同時調(diào)用同一個Position對象的Offset函數(shù),你就無法保證得到的結(jié)果是什么了。所以,為了保證線程安全,也許你還會想給Offset函數(shù)加上同步機制——麻煩了!
換個角度想想怎么樣?假如我們根本不讓Offset函數(shù)修改Position的內(nèi)容?假如我們讓Offset函數(shù)生成一個新的Position對象?如果是這樣,Position對象就已經(jīng)是線程安全的了——它沒有任何“寫”操作,而沒有寫操作的類是不需要同步的。于是我們這樣做了,并且很輕松的得到了一個線程安全的Position類。(見例2)
例2:線程安全的Position類(這里只展示Offset函數(shù))
Position Position::Offset(int offX, int offY){ //根據(jù)偏移量得到新的位置
return Position(x+offX, y+offY);
}
約束
- 你有一個天性被動的類。這個類的實例不需要改變自己的狀態(tài)。同時這個類的實例還被其他多個對象共享。
- 正確協(xié)調(diào)被共享的對象的狀態(tài)改變非常困難。當一個對象的狀態(tài)發(fā)生改變時,所有使用它的對象都應(yīng)該得到通知。這造成了對象之間的緊耦合。
- 在多線程共享時,還需要使用同步機制來保證線程安全性。
解決方案
為了避免狀態(tài)改變帶來的諸多麻煩,不允許對實例的狀態(tài)做任何修改。具體的做法就是:不在類的公開接口中出現(xiàn)任何可以修改對象狀態(tài)的方法,只出現(xiàn)狀態(tài)讀取方法。如果client需要不同的狀態(tài),就生成一個新的對象。(見圖1)

圖1 Immutable模式的類圖
效果
- 不再需要協(xié)調(diào)狀態(tài)修改的代碼,也不再需要協(xié)調(diào)任何同步代碼。
- 生成了更多的對象。增加了對象生成和銷毀的開銷。
實現(xiàn)
Immutable模式的實現(xiàn)主要有以下兩個要點:
1.除了構(gòu)造函數(shù)之外,不應(yīng)該有其它任何函數(shù)(至少是任何public函數(shù))修改任何成員變量。
2.任何使成員變量獲得新值的函數(shù)都應(yīng)該將新的值保存在新的對象中,而保持原來的對象不被修改。
在“效果”中我已經(jīng)講到:Immutable模式會大大提高對象生成和銷毀的頻率。因此,在C++中實現(xiàn)Immutable模式時,還必須特別注意對象的生存周期。你可以嘗試用智能指針[Meyers96, Item28]來幫助你處理對象的銷毀問題,但是無論如何你都必須仔細檢查以確保沒有內(nèi)存泄漏——如果每艘飛船的每次移動都會造成內(nèi)存泄漏,你的游戲該是多么糟糕!
此外,Immutable模式還有一種變體:Read Only Object模式。它的做法是:當一個類的對象對于某些client可寫、某些client不可寫時,讓這個類實現(xiàn)一個ReadOnly接口。然后讓可寫的client直接訪問對象,而讓不可寫的client通過ReadOnly接口訪問該對象,從而實現(xiàn)了不同的讀寫權(quán)限控制。(如圖2所示)

圖2 Read Only Object模式
Immutable模式與string類的實現(xiàn)策略
如果你也讀過[Meyers96],我想你一定對那個應(yīng)用在String類上的COW(Copy-On-Write)策略[Meyers96, Item29]印象深刻。COW策略是“l(fā)azy evaluation”的發(fā)展形式。如果對String類的寫操作數(shù)量很少,那么COW策略將大大提高整個String類的效率,并大大降低空間開銷。
可是你知道嗎?在STL中的string類并沒有采用COW策略,從例3就可以看出這一點。為什么?為什么這么好的策略沒有得到采用?相信你從[Meyers96]中便可發(fā)現(xiàn):實際在String類上實現(xiàn)COW策略是如此復雜。更何況我們還必須考慮線程安全的問題。我完全有理由認為:正是因為考慮到這些復雜的情況,STL的實現(xiàn)者們才最終決定用一個比較低效但是安全的實現(xiàn)方案。
例3:STL中的string::operator=和string::operator[]
//下面代碼出自SGI STL 2000年6月8日版本
//為了幫助讀者理解,我做了些微改動,并在關(guān)鍵位置加上注釋
//如果使用COW策略,operator=應(yīng)該不做內(nèi)容復制,而是進行引用計數(shù)
string& string::operator=(const string& s) {
if (&s != this)
assign(s.begin(),s.end()); // 這里的operator=只是簡單的內(nèi)容復制而已
return *this;
}
//如果使用COW策略,const的operator[]和非const的operator[]應(yīng)該不同
//但是這里兩個operator[]完全相同
const char & string::operator[](int n) const
{ return *(_M_start + n); } //_M_start是字符數(shù)組的起始位置
char & string::operator[](int n)
{ return *(_M_start + n); }
看到這些,我不能不開始猜想:為什么STL的設(shè)計者們一定要保留這些給他們造成麻煩的“修改函數(shù)”(即可以修改string內(nèi)容的函數(shù))?我想,這是因為他們希望讓string的行為方式盡量接近于C語言的char *型字符串。不然,我真的想不出其他任何保留operator[]的理由。
那么,如果不必非要讓string類的行為方式接近char *型字符串,如果string類的讀操作應(yīng)用頻率遠遠大于寫操作(在實際應(yīng)用中這是很常見的),你會考慮如何實現(xiàn)一個string類?啊,也許你已經(jīng)想到了:Immutable模式。你可以很舒服的使用[Meyers96]教你的引用計數(shù)方法來節(jié)約存儲空間,你不必再擔心寫操作的同步問題或別的什么,因為已經(jīng)沒有寫操作。任何改變字符串內(nèi)容的操作都將得到一個新的string對象。而對象生存期管理和存儲空間管理這兩個大問題也因為Immutable模式的引入而大大簡化,你完全可以參照[Meyers96]第183頁到第189頁的內(nèi)容自己來解決它們。
代碼示例
我用了一天的時間,做了一個簡單的ImmutableString實現(xiàn)。其中實現(xiàn)細節(jié)用了Proxy類[Meyers96],并參考了COM的引用計數(shù)規(guī)則[Pan99]。在這個例子中,讀者可以感覺到:Immutable模式大大簡化了共享空間的字符串類型的實現(xiàn),并為其中的一些方法(比如subString)的實現(xiàn)提供了非常大的便利。本來我想把代碼放在文章里面,但是時間和空間受限,最后決定放棄。
在該代碼中,我做了一個簡單的效率測試:反復進行字符串對象的賦值(operator=)操作。結(jié)果表明:ImmutableString的效率比std::string高出了一倍左右。假如你的業(yè)務(wù)就是不斷的讀取數(shù)據(jù)庫、不斷的賦值、不斷的輸出,而不對字符串進行修改,那么ImmutableString的效率提升是非常可觀的。
該示例代碼在VC .NET下編譯通過。
相關(guān)模式
經(jīng)常會使用Abstract Factory模式[GOF95]來創(chuàng)建新的對象。
大量的對象經(jīng)常通過Flyweight模式[GOF95]被共享。
參考書目
[Meyers96] Scott Meyers, More Effective C++, Addison-Wesley, 1996.
[GOF95] Erich Gamma etc., Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995. 中譯本:《設(shè)計模式:可復用面向?qū)ο筌浖幕A(chǔ)》,李英軍等譯,機械工業(yè)出版社,2000 年9月。
[PAN99] 潘愛民,《COM原理與應(yīng)用》,清華大學出版社,1999年11月。