本文大部分内容翻译至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些许修改,并将代码升级到了Swift2.0,翻译不当之处望多包涵。
原型模式(The prototype pattern)
原型模式多用于创建复杂的或者耗时的实例,因为这种情况下,复制一个已经存在的实例使程序运行更高效;或者创建值相等,只是命名不一样的同类数据。
理解
原型模式利用一个已经存在的对象,而非一个类或者一个结构体,去创建一个新的对象。这通常叫做克隆,因为新创建的对象是原型对象的一个完全复制,甚至包括在被复制之前对原型对象的存储属性的一些操作。
原型模式有三部操作:
- 第一,需要创建对象的组件请求复制原型对象
- 第二,原型对象的复制
- 第三,将复制的对象交给请求组件
值类型拷贝
当你将一个值类型赋值给变量时,Swift自动的实现了原型模式。值类型是用结构体来定义的,而且Swift所有内建的类型在幕后都是用结构体来实现的。这就意味着你能仅仅依靠赋值给变量来复制String,Boolean,collection,enumeration,tuple和numeric类型,Swift将会复制原型的值并且用它来创建一个克隆。下面例子来展示值类型是如何被克隆的。
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
对应的图例:
接着我们修改workMeeting的值:
workMeeting.name = "Alice"
workMeeting.day = "Fri"
workMeeting.place = "Conference Rm 2"
此时对应的图例:
如果我们输出的话,将会得到不同的结果:
Social with Alice on Fri at Conference Rm 2
Work with Alice on Fri at Conference Rm
引用类型拷贝
用类创建出的对象是引用类型,而且当你将它们赋值给变量的时候Swift并不会去复制它们。相反,关于这个对象的一个新的引用被创建了这样所有的变量都指向了同一个对象。
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")
除了用类关键字,同时还增加了一个初始化方法。Swift会为结构体创建一个默认的初始化方法但是类除外。除此之外,输出的结果也会和上面的结构体有所不同:
Social with Alice on Fri at Conference Rm 2
Work with Alice on Fri at Conference Rm 2
原因是这里只有一个对象,而且它们同时被beerMeeting和workMeeting所引用。
实现NSCopying协议
在面向对象编程里分配新的引用给已经存在的对象是一个重要的部分,但是对原型模式一点帮助也没有。为了实现对象的复制,Foundation框架定义了NSCopying协议。
NSCopying协议定义了copyWithZone方法,当对象被复制的时候会调用。为了复制Appointment,原型对象调用了copy方法(注意不是copyWithZone)。因为copyWithZone方法返回的是一个AnyObject,所以需要向下转型。值得注意的是,实现NSCopying协议并没有将引用类型转换成了值类型,所以必须调用copy方法去复制原型对象。如果你仅仅是将原型对象赋值给了一个新的变量,那么你只是获得了一个新的引用而非一个新的对象。
浅拷贝和深拷贝
原型模式的另一个重要的方面是对象被复制的时候用的是浅拷贝还是深拷贝。请看下面的例子:
又一次,在变量workMeeting上的修改影响了变量beerMeeting的值。这是因为我们将place属性的类型从String(值类型)变成了Location(引用类型),所以NSCopying协议对原型对象的Location属性创建了一个新的引用。这就是所谓的浅拷贝,对象的引用被拷贝了,而非对象本身。
实现深拷贝
为了实现深拷贝,不得不让Location实现NSCopying协议和继承NSObject类,并实现copyWithZone方法。所有你想实现深拷贝的引用类型都必须实现NSCopying协议,所以你必须对你原型对象相关的那些类重复这个操作,甚至是那些被其他引用关联的引用。
数组的拷贝
Swift的数组是通过结构体来实现的,这意味着它是值类型。当你将一个数组赋值给一个新的变量时,这个数组本身和它所包含的任何值类型都将被拷贝。数组包含的引用类型是浅拷贝,所以实际上原型数组和克隆数组包含的引用都是指向相同的对象。请看下面例子:
作为性能优化,Swift的数组只有去修改了它实际上才会被拷贝,这就是延迟拷贝(Lazy Copying)。这就意味着当你仅仅只是读取拷贝数组数据的时候,数组的拷贝就跟引用类型的拷贝一样;如果你改变拷贝数组的值,那么数组的拷贝就和值类型的拷贝一样。
实现数组的深拷贝
定义了一个叫做deepCopy的方法用来接受一个数组,并且用map方法去拷贝这个数组。传给map方法的闭包用来检查对象是否可以深拷贝,如果可以,调用copy方法。
原型模式的使用场景
1. 避免开销昂贵的实例初始化
使用NSCopying协议允许对象负责拷贝它们自己,这意味着可以避免开销很昂贵的初始化。请看下面的例子:
每一次创建Sum对象,都导致需要分配一个二维数组(注意到for循环执行了200次)。通过实现NSCopying协议,用原型模式可以解决这个问题。再看下面的例子:
上面改变了Sum类的声明,让它继承了NSObjec类(提供copy方法),同时实现NSCopying协议。为了能够克隆,还添加了一个新的初始化方法,用它来接受一个cached参数而并非去生成。我们还给这个初始化函数加上了private关键字,在保证了copyWithZone方法能够使用它的同时也阻止了其他组件去使用其他数据来初始化。
2. 将对象的创建和使用分离
原型模式允许组件不需要任何信息通过原型去创建新的对象,这意味着可以通过分离对象的创建和使用来减少和原始类(或者结构体)之间的耦合。实现原型模式的组件并不需要知道它们克隆的原型对象的类型,这就使得在那些需要创建新对象的组件中去限制大量关于子类的信息成为可能。这可能会有些难以理解,所以请看下面的例子:
上面定义了一个拥有属性to和subject的类Message。还有一个MessageLogger的类,它有一个存储Message对象的方法logMessage和一个接受闭包作为参数来处理存储的Message的方法processMessages。
问题出在我们为了优化再利用了已经创建的Message对象,这导致了MessageLogger里存储的Message都指向了同一个对象。
解决问题(并不是真正的)
如果对原型模式不熟悉又或者不喜欢NSCopying协议这种方式,那么你可能这么做:
揭示潜在的问题
上面的问题算是解决了,但是进一步的问题出现了。因为现在MessageLogger类希望通过调用Message类的初始化方法能创建Message对象。下面将通过Message类的子类来创建一个更加详细的DetailMessage类:
问题在于MessageLogger类同时接受了Message对象和DetailMessage对象,但是却只是将Message对象添加到了存储数组中。
解决问题(并不是真正的)
不使用原型模式的话,最明显的解决方法就是让MessageLogger类注意到现在有两种不同的对象了。请看下面:
这种方法通过在MessageLogger类里增加Message和Message子类的信息来解决问题,但是最大的问题是每当增加一个Message的子类都不得不去修改MessageLogger类。
应用原型模式
应用原型模式的优点在于对象能够保持和它们最开始被创建时完全一样来克隆,不论是Message对象或者它的子类对象。再创建新的子类或是修改类初始化方法的时候也不需要再去修改MessageLogger类。
总结
- 首先,要避免的是当需要深拷贝的时候却使用了浅拷贝。当克隆一个对象,要仔细的思考是否需要创建完全分离的对象的拷贝,或者仅仅是一个简单的引用已经足够了。创建引用相比深拷贝来说又快又简单,但是同时也意味着两个或者更多的引用指向了同一个对象。
- 不要强迫使用一个原型对象来创建所有的克隆。这会导致畸形的代码结构,将原型暴露给了App中的每一个组件。不要害怕在App的每一个逻辑章节中使用复数的原型,也不要忘了能通过克隆来创建对象。
- 实现原型模式的标准IOS方法是实现NSCopying协议和继承NSObject类。NSCopying协议对Swift来说可能并不友好,所以你可能会创建自己的协议或者构造方法(接受类实例并拷贝它的初始化方法)。这样也可以,但是使用NSCopying有一个好处就是很容易理解并且适用于IOS框架。不使用标准的协议将会限制你创建的原型模式的适用范围也将会使得和一些希望实现NSCopying协议的第三方代码的协作变得很困难。
Cocoa中的原型模式
原型模式贯穿整个Cocoa,尤其是在Foundation框架中,你会发现很多类都实现了NSCopying协议。
Cocoa数组
尤其感兴趣的是NSArray和它的子类NSMutableArray。使用这些类你将经常从Objective-C模组接收数据,而且它们和Swif中的数组也十分不同。看下面的例子:
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类创建的数组是不可变的。NSMutableArray是NSArray的子类,是可变数组。
这里我们创建了一个包含Int,String和一个Person对象的NSMutableArray的数组。Persion对象实现了NSCopying协议。(跟Swift中数组有所不同,NSMutableArray和NSArray都不是强类型的)
我们将这个数组赋值给一个新的变量copiedData ,并且修改了数组中每个元素的值。为了完成这个例子,我们用Swift的恒等式(===)去弄清楚data和copiedData是否引用了同一个对象。最后得到下面的结果:
Identity: true
0: 20 1: MacOS 2: Alice
Swift中的数组使用结构体来实现的,这意味着当你将一个Swift数组赋值给一个新的变量时实际上是创造了一个新的数组并且复制了数组中的值。但是这里却不一样,相反,改变其中一个变量的值却同时对两边产生了影响。NSArray和NSMutableArray其实都是引用类型,所以和Swift中的Array产生了不同的行为。
Cocoa数组的浅拷贝
我们能应用原型模式并且拷贝数组,当然仅仅是浅拷贝。除了copy方法以外,其实在Foundation类中还有一个和原型模式相关的方法mutableCopy。
Copy:返回一个NSArray实例,不能被修改
mutableCopy:返回一个NSMutableArray实例,可以被修改
Cocoa或者Objective-C中的这些方法和Swift中用let和var关键字来声明可变和不可变数组形成了一个冲突。下面将展示如何用mutableCopy方法来克隆NSMutableArray产生另外一个对象。
...
var data = NSMutableArray(objects: 10, "iOS", Person(name:"Joe", country:"USA"))
var copiedData = data.mutableCopy() as NSArray
...
这样的结果就是产生了两个不同的NSMutableArray对象。数组的元素是浅拷贝,所以虽然值类型被复制了但是两个数组引用了同一个Persion对象。结果就是这样:
Identity: false
0: 10 1: iOS 2: Alice
Cocoa数组的深拷贝
NSArray和NSMutableArray定义了复制数组的构造方法,并且可选择的深拷贝一个原型数组的元素。
...
var data = NSMutableArray(objects: 10, "iOS", Person(name:"Joe", country:"USA"))
var copiedData = NSMutableArray(array: data as [AnyObject], copyItems: true)
...
复制构造方法的参数是一个原型数组和一个指定实现了NSCopyable(NSCopying)协议的对象是否被克隆的布尔类型的值。这里指定了ture,所以现在引用类型的对象也被深拷贝了。
Identity: false
0: 10 1: iOS 2: Joe
使用NSCopying属性标注
Swift支持标注来改变属性的行为。其中之一就是@NSCopying,可以应用于任何来源于NSObject并且实现NSCopying协议的类型的存储属性来调用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实际上继承了NSObject并且实现了NSCopying协议
在这个例子中,我们定义了一个含有可选变量from和data的类LogItem。我们给data标注上了@NSCopying所以当data属性被设定的时候它实际上是支持浅拷贝的。所以会看到下面的结果:
Value: 2
关于@NSCopying有一些限制。
- 首先是在初始化方法里面值被设定的时候是不会克隆的,这也就是为什么我们会将data定义为可选类型所以我们不用在初始化方法里面去设定它。
- 另一个是@NSCopying会去调用copy方法,甚至当对象支持mutableCopy方法的时候。这就意味着当我们把NSMutableArray对象赋值给LogItem的data属性时实际上它被转换成了不可变的NSArray对象,阻止我们做进一步的修改。