本文大部分內(nèi)容翻譯至《Pro Design Pattern In Swift》By Adam Freeman语淘,一些地方做了些許修改巷蚪,并將代碼升級(jí)到了Swift2.0,翻譯不當(dāng)之處望多包涵杭措。
原型模式(The prototype pattern)
原型模式多用于創(chuàng)建復(fù)雜的或者耗時(shí)的實(shí)例柄沮,因?yàn)檫@種情況下审孽,復(fù)制一個(gè)已經(jīng)存在的實(shí)例使程序運(yùn)行更高效午阵;或者創(chuàng)建值相等躺孝,只是命名不一樣的同類數(shù)據(jù)。
理解
原型模式利用一個(gè)已經(jīng)存在的對(duì)象底桂,而非一個(gè)類或者一個(gè)結(jié)構(gòu)體植袍,去創(chuàng)建一個(gè)新的對(duì)象。這通常叫做克隆籽懦,因?yàn)樾聞?chuàng)建的對(duì)象是原型對(duì)象的一個(gè)完全復(fù)制于个,甚至包括在被復(fù)制之前對(duì)原型對(duì)象的存儲(chǔ)屬性的一些操作。
原型模式有三部操作:
- 第一暮顺,需要?jiǎng)?chuàng)建對(duì)象的組件請(qǐng)求復(fù)制原型對(duì)象
- 第二厅篓,原型對(duì)象的復(fù)制
- 第三秀存,將復(fù)制的對(duì)象交給請(qǐng)求組件
值類型拷貝
當(dāng)你將一個(gè)值類型賦值給變量時(shí),Swift自動(dòng)的實(shí)現(xiàn)了原型模式羽氮。值類型是用結(jié)構(gòu)體來(lái)定義的或链,而且Swift所有內(nèi)建的類型在幕后都是用結(jié)構(gòu)體來(lái)實(shí)現(xiàn)的。這就意味著你能僅僅依靠賦值給變量來(lái)復(fù)制String,Boolean,collection,enumeration,tuple和numeric類型,Swift將會(huì)復(fù)制原型的值并且用它來(lái)創(chuàng)建一個(gè)克隆档押。下面例子來(lái)展示值類型是如何被克隆的澳盐。
struct Appointment{
var name:String
var day:String
var place:String
func printDetails(label:String){
print("\(label) with \(name) on \(day) at \(place)")
}
}
var beerMeeting = Appointment(name: "Bob", day: "Mon", place: "Joe's Bar")
var workMeeting = beerMeeting
對(duì)應(yīng)的圖例:
接著我們修改workMeeting的值:
workMeeting.name = "Alice"
workMeeting.day = "Fri"
workMeeting.place = "Conference Rm 2"
此時(shí)對(duì)應(yīng)的圖例:
如果我們輸出的話,將會(huì)得到不同的結(jié)果:
Social with Alice on Fri at Conference Rm 2
Work with Alice on Fri at Conference Rm
引用類型拷貝
用類創(chuàng)建出的對(duì)象是引用類型汇荐,而且當(dāng)你將它們賦值給變量的時(shí)候Swift并不會(huì)去復(fù)制它們洞就。相反,關(guān)于這個(gè)對(duì)象的一個(gè)新的引用被創(chuàng)建了這樣所有的變量都指向了同一個(gè)對(duì)象掀淘。
class Appointment {
var name:String
var day:String
var place:String
init(name:String, day:String, place:String) {
self.name = name
self.day = day
self.place = place
}
func printDetails(label:String) {
print("\(label) with \(name) on \(day) at \(place)")
}
}
var beerMeeting = Appointment(name: "Bob", day: "Mon", place: "Joe's >Bar")
var workMeeting = beerMeeting
workMeeting.name = "Alice"
workMeeting.day = "Fri"
workMeeting.place = "Conference Rm 2"
beerMeeting.printDetails("Social")
workMeeting.printDetails("Work")
除了用類關(guān)鍵字旬蟋,同時(shí)還增加了一個(gè)初始化方法。Swift會(huì)為結(jié)構(gòu)體創(chuàng)建一個(gè)默認(rèn)的初始化方法但是類除外革娄。除此之外倾贰,輸出的結(jié)果也會(huì)和上面的結(jié)構(gòu)體有所不同:
Social with Alice on Fri at Conference Rm 2
Work with Alice on Fri at Conference Rm 2
原因是這里只有一個(gè)對(duì)象,而且它們同時(shí)被beerMeeting和workMeeting所引用拦惋。
實(shí)現(xiàn)NSCopying協(xié)議
在面向?qū)ο缶幊汤锓峙湫碌囊媒o已經(jīng)存在的對(duì)象是一個(gè)重要的部分匆浙,但是對(duì)原型模式一點(diǎn)幫助也沒(méi)有。為了實(shí)現(xiàn)對(duì)象的復(fù)制厕妖,F(xiàn)oundation框架定義了NSCopying協(xié)議首尼。
NSCopying協(xié)議定義了copyWithZone方法,當(dāng)對(duì)象被復(fù)制的時(shí)候會(huì)調(diào)用言秸。為了復(fù)制Appointment软能,原型對(duì)象調(diào)用了copy方法(注意不是copyWithZone)。因?yàn)閏opyWithZone方法返回的是一個(gè)AnyObject举畸,所以需要向下轉(zhuǎn)型查排。值得注意的是,實(shí)現(xiàn)NSCopying協(xié)議并沒(méi)有將引用類型轉(zhuǎn)換成了值類型抄沮,所以必須調(diào)用copy方法去復(fù)制原型對(duì)象跋核。如果你僅僅是將原型對(duì)象賦值給了一個(gè)新的變量,那么你只是獲得了一個(gè)新的引用而非一個(gè)新的對(duì)象叛买。
淺拷貝和深拷貝
原型模式的另一個(gè)重要的方面是對(duì)象被復(fù)制的時(shí)候用的是淺拷貝還是深拷貝砂代。請(qǐng)看下面的例子:
又一次,在變量workMeeting上的修改影響了變量beerMeeting的值率挣。這是因?yàn)槲覀儗lace屬性的類型從String(值類型)變成了Location(引用類型)泊藕,所以NSCopying協(xié)議對(duì)原型對(duì)象的Location屬性創(chuàng)建了一個(gè)新的引用。這就是所謂的淺拷貝,對(duì)象的引用被拷貝了娃圆,而非對(duì)象本身玫锋。
實(shí)現(xiàn)深拷貝
為了實(shí)現(xiàn)深拷貝,不得不讓Location實(shí)現(xiàn)NSCopying協(xié)議和繼承NSObject類讼呢,并實(shí)現(xiàn)copyWithZone方法撩鹿。所有你想實(shí)現(xiàn)深拷貝的引用類型都必須實(shí)現(xiàn)NSCopying協(xié)議,所以你必須對(duì)你原型對(duì)象相關(guān)的那些類重復(fù)這個(gè)操作悦屏,甚至是那些被其他引用關(guān)聯(lián)的引用节沦。
數(shù)組的拷貝
Swift的數(shù)組是通過(guò)結(jié)構(gòu)體來(lái)實(shí)現(xiàn)的,這意味著它是值類型础爬。當(dāng)你將一個(gè)數(shù)組賦值給一個(gè)新的變量時(shí)甫贯,這個(gè)數(shù)組本身和它所包含的任何值類型都將被拷貝。數(shù)組包含的引用類型是淺拷貝看蚜,所以實(shí)際上原型數(shù)組和克隆數(shù)組包含的引用都是指向相同的對(duì)象叫搁。請(qǐng)看下面例子:
作為性能優(yōu)化,Swift的數(shù)組只有去修改了它實(shí)際上才會(huì)被拷貝供炎,這就是延遲拷貝(Lazy Copying)渴逻。這就意味著當(dāng)你僅僅只是讀取拷貝數(shù)組數(shù)據(jù)的時(shí)候,數(shù)組的拷貝就跟引用類型的拷貝一樣音诫;如果你改變拷貝數(shù)組的值惨奕,那么數(shù)組的拷貝就和值類型的拷貝一樣。
實(shí)現(xiàn)數(shù)組的深拷貝
定義了一個(gè)叫做deepCopy的方法用來(lái)接受一個(gè)數(shù)組竭钝,并且用map方法去拷貝這個(gè)數(shù)組梨撞。傳給map方法的閉包用來(lái)檢查對(duì)象是否可以深拷貝,如果可以香罐,調(diào)用copy方法卧波。
原型模式的使用場(chǎng)景
1. 避免開(kāi)銷昂貴的實(shí)例初始化
使用NSCopying協(xié)議允許對(duì)象負(fù)責(zé)拷貝它們自己,這意味著可以避免開(kāi)銷很昂貴的初始化穴吹。請(qǐng)看下面的例子:
每一次創(chuàng)建Sum對(duì)象幽勒,都導(dǎo)致需要分配一個(gè)二維數(shù)組(注意到for循環(huán)執(zhí)行了200次)嗜侮。通過(guò)實(shí)現(xiàn)NSCopying協(xié)議港令,用原型模式可以解決這個(gè)問(wèn)題。再看下面的例子:
上面改變了Sum類的聲明锈颗,讓它繼承了NSObjec類(提供copy方法)顷霹,同時(shí)實(shí)現(xiàn)NSCopying協(xié)議。為了能夠克隆击吱,還添加了一個(gè)新的初始化方法淋淀,用它來(lái)接受一個(gè)cached參數(shù)而并非去生成。我們還給這個(gè)初始化函數(shù)加上了private關(guān)鍵字覆醇,在保證了copyWithZone方法能夠使用它的同時(shí)也阻止了其他組件去使用其他數(shù)據(jù)來(lái)初始化朵纷。
2. 將對(duì)象的創(chuàng)建和使用分離
原型模式允許組件不需要任何信息通過(guò)原型去創(chuàng)建新的對(duì)象炭臭,這意味著可以通過(guò)分離對(duì)象的創(chuàng)建和使用來(lái)減少和原始類(或者結(jié)構(gòu)體)之間的耦合。實(shí)現(xiàn)原型模式的組件并不需要知道它們克隆的原型對(duì)象的類型袍辞,這就使得在那些需要?jiǎng)?chuàng)建新對(duì)象的組件中去限制大量關(guān)于子類的信息成為可能鞋仍。這可能會(huì)有些難以理解,所以請(qǐng)看下面的例子:
上面定義了一個(gè)擁有屬性to和subject的類Message搅吁。還有一個(gè)MessageLogger的類威创,它有一個(gè)存儲(chǔ)Message對(duì)象的方法logMessage和一個(gè)接受閉包作為參數(shù)來(lái)處理存儲(chǔ)的Message的方法processMessages。
問(wèn)題出在我們?yōu)榱藘?yōu)化再利用了已經(jīng)創(chuàng)建的Message對(duì)象谎懦,這導(dǎo)致了MessageLogger里存儲(chǔ)的Message都指向了同一個(gè)對(duì)象肚豺。
解決問(wèn)題(并不是真正的)
如果對(duì)原型模式不熟悉又或者不喜歡NSCopying協(xié)議這種方式,那么你可能這么做:
揭示潛在的問(wèn)題
上面的問(wèn)題算是解決了界拦,但是進(jìn)一步的問(wèn)題出現(xiàn)了吸申。因?yàn)楝F(xiàn)在MessageLogger類希望通過(guò)調(diào)用Message類的初始化方法能創(chuàng)建Message對(duì)象。下面將通過(guò)Message類的子類來(lái)創(chuàng)建一個(gè)更加詳細(xì)的DetailMessage類:
問(wèn)題在于MessageLogger類同時(shí)接受了Message對(duì)象和DetailMessage對(duì)象寞奸,但是卻只是將Message對(duì)象添加到了存儲(chǔ)數(shù)組中呛谜。
解決問(wèn)題(并不是真正的)
不使用原型模式的話,最明顯的解決方法就是讓MessageLogger類注意到現(xiàn)在有兩種不同的對(duì)象了枪萄。請(qǐng)看下面:
這種方法通過(guò)在MessageLogger類里增加Message和Message子類的信息來(lái)解決問(wèn)題隐岛,但是最大的問(wèn)題是每當(dāng)增加一個(gè)Message的子類都不得不去修改MessageLogger類。
應(yīng)用原型模式
應(yīng)用原型模式的優(yōu)點(diǎn)在于對(duì)象能夠保持和它們最開(kāi)始被創(chuàng)建時(shí)完全一樣來(lái)克隆瓷翻,不論是Message對(duì)象或者它的子類對(duì)象聚凹。再創(chuàng)建新的子類或是修改類初始化方法的時(shí)候也不需要再去修改MessageLogger類。
總結(jié)
- 首先齐帚,要避免的是當(dāng)需要深拷貝的時(shí)候卻使用了淺拷貝妒牙。當(dāng)克隆一個(gè)對(duì)象,要仔細(xì)的思考是否需要?jiǎng)?chuàng)建完全分離的對(duì)象的拷貝对妄,或者僅僅是一個(gè)簡(jiǎn)單的引用已經(jīng)足夠了湘今。創(chuàng)建引用相比深拷貝來(lái)說(shuō)又快又簡(jiǎn)單,但是同時(shí)也意味著兩個(gè)或者更多的引用指向了同一個(gè)對(duì)象剪菱。
- 不要強(qiáng)迫使用一個(gè)原型對(duì)象來(lái)創(chuàng)建所有的克隆摩瞎。這會(huì)導(dǎo)致畸形的代碼結(jié)構(gòu),將原型暴露給了App中的每一個(gè)組件孝常。不要害怕在App的每一個(gè)邏輯章節(jié)中使用復(fù)數(shù)的原型旗们,也不要忘了能通過(guò)克隆來(lái)創(chuàng)建對(duì)象。
- 實(shí)現(xiàn)原型模式的標(biāo)準(zhǔn)IOS方法是實(shí)現(xiàn)NSCopying協(xié)議和繼承NSObject類构灸。NSCopying協(xié)議對(duì)Swift來(lái)說(shuō)可能并不友好上渴,所以你可能會(huì)創(chuàng)建自己的協(xié)議或者構(gòu)造方法(接受類實(shí)例并拷貝它的初始化方法)。這樣也可以,但是使用NSCopying有一個(gè)好處就是很容易理解并且適用于IOS框架稠氮。不使用標(biāo)準(zhǔn)的協(xié)議將會(huì)限制你創(chuàng)建的原型模式的適用范圍也將會(huì)使得和一些希望實(shí)現(xiàn)NSCopying協(xié)議的第三方代碼的協(xié)作變得很困難曹阔。
Cocoa中的原型模式
原型模式貫穿整個(gè)Cocoa,尤其是在Foundation框架中隔披,你會(huì)發(fā)現(xiàn)很多類都實(shí)現(xiàn)了NSCopying協(xié)議次兆。
Cocoa數(shù)組
尤其感興趣的是NSArray和它的子類NSMutableArray。使用這些類你將經(jīng)常從Objective-C模組接收數(shù)據(jù)锹锰,而且它們和Swif中的數(shù)組也十分不同芥炭。看下面的例子:
import Foundation
class Person : NSObject, NSCopying {
var name:String
var country: String
init(name:String, country:String) {
self.name = name; self.country = country;
}
func copyWithZone(zone: NSZone) -> AnyObject {
return Person(name: self.name, country: self.country);
}
}
var data = NSMutableArray(objects: 10, "iOS", Person(name:"Joe", country:"USA"))
var copiedData = data
data[0] = 20
data[1] = "MacOS"
(data[2] as! Person).name = "Alice"
print("Identity: \(data === copiedData)")
print("0: \(copiedData[0]) 1: \(copiedData[1]) 2: \(copiedData[2].name)")
Tip:NSArray類創(chuàng)建的數(shù)組是不可變的恃慧。NSMutableArray是NSArray的子類园蝠,是可變數(shù)組。
這里我們創(chuàng)建了一個(gè)包含Int痢士,String和一個(gè)Person對(duì)象的NSMutableArray的數(shù)組彪薛。Persion對(duì)象實(shí)現(xiàn)了NSCopying協(xié)議。(跟Swift中數(shù)組有所不同怠蹂,NSMutableArray和NSArray都不是強(qiáng)類型的)
我們將這個(gè)數(shù)組賦值給一個(gè)新的變量copiedData 善延,并且修改了數(shù)組中每個(gè)元素的值。為了完成這個(gè)例子城侧,我們用Swift的恒等式(===)去弄清楚data和copiedData是否引用了同一個(gè)對(duì)象易遣。最后得到下面的結(jié)果:
Identity: true
0: 20 1: MacOS 2: Alice
Swift中的數(shù)組使用結(jié)構(gòu)體來(lái)實(shí)現(xiàn)的,這意味著當(dāng)你將一個(gè)Swift數(shù)組賦值給一個(gè)新的變量時(shí)實(shí)際上是創(chuàng)造了一個(gè)新的數(shù)組并且復(fù)制了數(shù)組中的值嫌佑。但是這里卻不一樣豆茫,相反,改變其中一個(gè)變量的值卻同時(shí)對(duì)兩邊產(chǎn)生了影響屋摇。NSArray和NSMutableArray其實(shí)都是引用類型揩魂,所以和Swift中的Array產(chǎn)生了不同的行為。
Cocoa數(shù)組的淺拷貝
我們能應(yīng)用原型模式并且拷貝數(shù)組炮温,當(dāng)然僅僅是淺拷貝火脉。除了copy方法以外,其實(shí)在Foundation類中還有一個(gè)和原型模式相關(guān)的方法mutableCopy柒啤。
Copy:返回一個(gè)NSArray實(shí)例倦挂,不能被修改
mutableCopy:返回一個(gè)NSMutableArray實(shí)例,可以被修改
Cocoa或者Objective-C中的這些方法和Swift中用let和var關(guān)鍵字來(lái)聲明可變和不可變數(shù)組形成了一個(gè)沖突白修。下面將展示如何用mutableCopy方法來(lái)克隆NSMutableArray產(chǎn)生另外一個(gè)對(duì)象妒峦。
...
var data = NSMutableArray(objects: 10, "iOS", Person(name:"Joe", country:"USA"))
var copiedData = data.mutableCopy() as NSArray
...
這樣的結(jié)果就是產(chǎn)生了兩個(gè)不同的NSMutableArray對(duì)象重斑。數(shù)組的元素是淺拷貝兵睛,所以雖然值類型被復(fù)制了但是兩個(gè)數(shù)組引用了同一個(gè)Persion對(duì)象。結(jié)果就是這樣:
Identity: false
0: 10 1: iOS 2: Alice
Cocoa數(shù)組的深拷貝
NSArray和NSMutableArray定義了復(fù)制數(shù)組的構(gòu)造方法,并且可選擇的深拷貝一個(gè)原型數(shù)組的元素祖很。
...
var data = NSMutableArray(objects: 10, "iOS", Person(name:"Joe", country:"USA"))
var copiedData = NSMutableArray(array: data as [AnyObject], copyItems: true)
...
復(fù)制構(gòu)造方法的參數(shù)是一個(gè)原型數(shù)組和一個(gè)指定實(shí)現(xiàn)了NSCopyable(NSCopying)協(xié)議的對(duì)象是否被克隆的布爾類型的值笛丙。這里指定了ture,所以現(xiàn)在引用類型的對(duì)象也被深拷貝了假颇。
Identity: false
0: 10 1: iOS 2: Joe
使用NSCopying屬性標(biāo)注
Swift支持標(biāo)注來(lái)改變屬性的行為胚鸯。其中之一就是@NSCopying,可以應(yīng)用于任何來(lái)源于NSObject并且實(shí)現(xiàn)NSCopying協(xié)議的類型的存儲(chǔ)屬性來(lái)調(diào)用copy方法笨鸡。
import Foundation
class LogItem {
var from:String?
@NSCopying var data:NSArray?
}
var dataArray = NSMutableArray(array: [1, 2, 3, 4])
var logitem = LogItem()
logitem.from = "Alice"
logitem.data = dataArray
dataArray[1] = 10
print("Value: \(logitem.data![1])")
這里的data是NSArray類型姜钳,NSArray實(shí)際上繼承了NSObject并且實(shí)現(xiàn)了NSCopying協(xié)議
在這個(gè)例子中,我們定義了一個(gè)含有可選變量from和data的類LogItem形耗。我們給data標(biāo)注上了@NSCopying所以當(dāng)data屬性被設(shè)定的時(shí)候它實(shí)際上是支持淺拷貝的哥桥。所以會(huì)看到下面的結(jié)果:
Value: 2
關(guān)于@NSCopying有一些限制。
- 首先是在初始化方法里面值被設(shè)定的時(shí)候是不會(huì)克隆的激涤,這也就是為什么我們會(huì)將data定義為可選類型所以我們不用在初始化方法里面去設(shè)定它拟糕。
- 另一個(gè)是@NSCopying會(huì)去調(diào)用copy方法,甚至當(dāng)對(duì)象支持mutableCopy方法的時(shí)候倦踢。這就意味著當(dāng)我們把NSMutableArray對(duì)象賦值給LogItem的data屬性時(shí)實(shí)際上它被轉(zhuǎn)換成了不可變的NSArray對(duì)象送滞,阻止我們做進(jìn)一步的修改。