原型模式
介紹
在許多面向對象的應用程序中,有些對象的創建代價過大或者過于復雜。要是可以重建相同的對象并作輕微的改動,事情就會容易很多。典型的例子就是復制組合結構(比如樹型結構)。從零構建一個樹型組合體非常困難,我們可以通過輕微改動重用已有的對象,以適應程序中的特定狀況。
在使用原型模式時,我們需要首先創建一個原型對象,再通過復制這個原型對象來創建更多同類型的對象。
定義
使用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。原型模式是一種對象創建型模式。
原型模式的工作原理很簡單:將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象拷貝自己來實現創建過程。由于在軟件系統中我們經常會遇到需要創建多個相同或者相似對象的情
需要注意的是通過克隆方法所創建的對象是全新的對象,它們在內存中擁有新的地址,通常對克隆所產生的對象進行修改對原型對象不會造成任何影響,每一個克隆對象都是相互獨立的。通過不同的方式修改可以得到一系列相似但不完全相同的對象。
UML類圖
Prototype聲明了復制自身的接口,作為prototype的子類,ConcretePrototype實現了自身的clone操作。
角色介紹
- Prototype(抽象原型類):它是聲明克隆方法的接口,是所有具體原型類的公共父類,可以是抽象類也可以是接口,甚至還可以是具體實現類。
- ConcretePrototype(具體原型類):它實現在抽象原型類中聲明的克隆方法,在克隆方法中返回自己的一個克隆對象。
- Client(客戶類):讓一個原型對象克隆自身從而創建一個新的對象,在客戶類中只需要直接實例化或通過工廠方法等方式創建一個原型對象,再通過調用該對象的克隆方法即可得到多個相同的對象。由于客戶類針對抽象原型類Prototype編程,因此用戶可以根據需要選擇具體原型類,系統具有較好的可擴展性,增加或更換具體原型類都很方便。
實現
class ConcretePrototype implements Prototype
{
private String attr; //成員屬性
public void setAttr(String attr)
{
this.attr = attr;
}
public String getAttr()
{
return this.attr;
}
public Prototype clone() //克隆方法
{
Prototype prototype = new ConcretePrototype(); //創建新對象
prototype.setAttr(this.attr);
return prototype;
}
}
客戶端調用
Prototype obj1 = new ConcretePrototype();
obj1.setAttr("Sunny");
Prototype obj2 = obj1.clone();
克隆
淺克隆
在淺克隆中,如果原型對象的成員變量是值類型,將復制一份給克隆對象;如果原型對象的成員變量是引用類型,則將引用對象的地址復制一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。簡單來說,在淺克隆中,當對象被復制時只復制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有復制。
在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆。
c中的淺拷貝就是對內存地址的復制,讓目標對象指針和源對象指向同一片內存空間。如:
char *str = (char*)malloc(100);
char* str2 = str;
淺拷貝只是對對象的簡單拷貝,讓幾個對象共用同一片內存,當內存銷毀時,指向這片內存的幾個指針需要重新定義才可以使用,要不然成為野指針。
深克隆
在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都將復制一份給克隆對象,深克隆將原型對象的所有引用對象也復制一份給克隆對象。簡單來說,在深克隆中,除了對象本身被復制外,對象所包含的所有成員變量也將復制。
在Java語言中,如果需要實現深克隆,可以通過序列化(Serialization)等方式來實現。序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在于內存中。通過序列化實現的拷貝不僅可以復制對象本身,而且可以復制其引用的成員對象,因此通過序列化將對象寫到一個流中,再從流里將其讀出來,可以實現深克隆。需要注意的是能夠實現序列化的對象其類必須實現Serializable接口,否則無法實現序列化操作。
c中的深拷貝是指拷貝對象的內容也重新分配一塊地址,兩個對象也互不影響、互不干涉。如:
char *str = (char*)malloc(100);
char *str2 = (char*)malloc(100);
memcpy(str2, str, sizeof(char)* 100);
Cocoa中對象的復制
Cocoa為NSObject的派生類提供了實現深復制的協議。NSObject的子類需要實現NSCopying協議及其方法-(id)copyWithZone:(NSZone *)zone。NSObject有一個實例方法copy,默認的copy方法內部調用[self copyWithZone:nil]。對于采納了NSCopying協議的子類,必須要實現這個copyWithZone方法,否則會發生異常.
iOS中普通對象的拷貝
- 若沒有實現過copyWithZone, 則可直接使用alloc和init創建新對象;
- 若該類繼承的父類實現了copyWithZone方法,則需overwrite該方法,先調用父類的該方法,再對新增的變量賦值;
- 若為immutable對象,則直接retain+1,返回原始對象;(如同NSString)
demo
@implementation TestObj
-(id)copyWithZone:(NSZone *)zone
{
Tire *copy = [[[self class] allocWithZone:zone] init];
return copy;
}
@end
子類實現copying協議,先調用父類
-(id)copyWithZone: (NSZone *) zone
{
AllWeatherRadial *tireCopy;
tireCopy = [super copyWithZone : zone];
tireCopy.rainHandling = rainHandling;
tireCopy.snowHandling = snowHandling;
return tireCopy;
}
NSMutableCopying
對于mutable對象,實現的是NSMutableCopying協議
如果該類的父類實現了mutableCopyWithZone:方法, 則需要重寫該方法,先調用父類的該方法,再對新增的變量賦值;
iOS中集合對象的淺拷貝
NSArray *shallowCopyArray = [someArray copyWithZone:nil];
// or
NSDictionary *shallowCopyDict = [[NSDictioary alloc] initWitheDictionary:someDictioanry copyItems: NO];
iOS中集合對象的深拷貝
NSArray *deepCopyArray = [NSArray alloc] initWitheArray: someArray copyItems: YES]; // 只深拷貝了一層,如果數組的元素也為 集合類型, 則無法完全拷貝
// or
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: oldArray]]; // 完全拷貝
iOS中NSString、NSMutableString對象
NSString對象的retain非常特殊。
總結起來是:
@式:
NSString *s = @"t";
NSLog(@"s:%lx",[s retainCount]);//輸出值為0xffffffffffffffff(UINT_MAX, 2147483647)
NSLog(@"s:%ld",[s retainCount]);//輸出值-1,由于0xffffffffffffffff補碼表示的值為-1
s指向的是字符串常量(類似于c語言中的靜態區存儲),系統不做引用計數,任何retain\release操作,其retainCount值都為UINT_MAX。
stringWithFormat:
NSString *s = [NSString stringWithFormat:@"%s", "test"];
NSLog(@"s:%d",[s retainCount]); //輸出值為1
系統會正常使用引用計數,和普通對象一樣;
stringWithString:
取決于后面的string對象;
NSString *s1 = [NSString stringWithString:@"test"];
NSLog(@"s1:%d",[s1 retainCount]); // 2147483647
如“test”為常量,則s1也指向常量,retain為UINT_MAX;
NSString *s2 = [NSString stringWithString:[NSString stringWithFormat:@"test,%d",1]];
NSLog(@"s2:%d",[s2 retainCount]); // 2
如先用stringWithFormat生成了一個變量,retain為1,再用stringWithString,retain增為2.
NSMutableString
NSMutableString* myStr3 = [NSMutableString stringWithString:@"string 3"]; // 1
使用stringWithString,正常計數。
因此 這里只對使用stringWithFormat式創建的對象的copy/mutblecopy行為進行研究。
T* source = [T stringWithFormat: @"test"];
T* dest = [source copy/mutablecopy];
使用copy,無論source\dest對象是否為mutable,都會退化為immutable;且執行的是淺拷貝,指向同一個老的對象, retain數+1;
使用mutablecopy,只有在source和dest都為NSMutableString時,可正常使用;且執行的是深拷貝,生成新對象,retain數為1。
iOS拷貝小總計
總結
優點
- 當創建新的對象實例較為復雜時,使用原型模式可以簡化對象的創建過程,通過復制一個已有實例可以提高新實例的創建效率。
- 擴展性較好,由于在原型模式中提供了抽象原型類,在客戶端可以針對抽象原型類進行編程,而將具體原型類寫在配置文件中,增加或減少產品類對原有系統都沒有任何影響。
- 原型模式提供了簡化的創建結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不需要這樣,原型模式中產品的復制是通過封裝在原型類中的克隆方法實現的,無須專門的工廠類來創建產品。
- 可以使用深克隆的方式保存對象的狀態,使用原型模式將對象復制一份并將其狀態保存起來,以便在需要的時候使用(如恢復到某一歷史狀態),可輔助實現撤銷操作。
缺點
- 需要為每一個類配備一個克隆方法,而且該克隆方法位于一個類的內部,當對已有的類進行改造時,需要修改源代碼,違背了“開閉原則”。
- 在實現深克隆時需要編寫較為復雜的代碼,而且當對象之間存在多重的嵌套引用時,為了實現深克隆,每一層對象對應的類都必須支持深克隆,實現起來可能會比較麻煩。
適用場景
- 創建新對象成本較大(如初始化需要占用較長的時間,占用太多的CPU資源或網絡資源),新的對象可以通過原型模式對已有對象進行復制來獲得,如果是相似對象,則可以對其成員變量稍作修改。
- 如果系統要保存對象的狀態,而對象的狀態變化很小,或者對象本身占用內存較少時,可以使用原型模式配合備忘錄模式來實現。
- 需要避免使用分層次的工廠類來創建分層次的對象,并且類的實例對象只有一個或很少的幾個組合狀態,通過復制原型對象得到新實例可能比使用構造函數創建一個新實例更加方便。