啥叫原型模式?
创建一个新的对象,然后通过复制现有的对象,称为原型。
原型本身使用模板创建,后续实例是克隆模板产生的。
问题 | 答案 |
---|---|
这是什么? | 通过复制现有的对象,称为原型,原型模式创建新的对象。 |
有哪些好处? | 主要的好处就是隐藏代码,创建对象的组件,使用它们;这意味着组件不需要知道哪个类或创建一个新的对象所需的结构,不需要知道细节的初始值设定项,而不需要更改当子类创建和实例化。这种模式也可以用来避免重复昂贵初始化每次创建一个新的对象的特定类型。 |
什么时候用这种模式? | 当你正在编写一个组件,需要创建对象的新实例,而无需在类初始值设定项创建依赖项时,此模式非常有用。 |
何时应该避免使用这种模式? | 有使用此模式,没有任何缺点。 |
如何知道你使用了这个模式? | 若要测试此模式的有效实现,请更改类或结构用于原型对象并检查是否作相应的改变是需要创建克隆的组件中的初始值设定项。作为第二个测试创建原型的类的子类,并确保组件可以克隆它无需进行任何更改。 |
是否有任何常见的陷阱? | 最主要的陷阱选择复制克隆的原型对象时的错误样式。有两种的复制可用 — — 浅层和深层 — — 也是很重要,要选择那种合适您的应用程序。请参阅"理解浅层和深层复制"部分了解详细信息。 |
有任何相关的模式吗? | 最密切相关的模式是描述对象模板模式。单例模式,提供一种手段,可以共享单个对象,避免需要创建其他实例。 |
理解模式所处理的问题
当我们使用模板来创建对象,但这是一个办法,有其自身的缺陷。
初始化消耗过大
一些类或结构的模板消耗非常大,意思初始化对象的一个新实例可以消耗大量的内存或计算为准备使用的对象。为了证明这种问题,下面的代码。
class Sum {
var resultsCache: [[Int]]
var firstValue:Int
var secondValue:Int
init(first:Int, second:Int) {
resultsCache = [[Int]](count: 10, repeatedValue:[Int](count:10, repeatedValue: 0))
for i in 0..<10 {
for j in 0..<10 {
self.resultsCache[i][j] = i + j;
}
}
self.firstValue = first
self.secondValue = second
}
var Result:Int {
get {
return firstValue < resultsCache.count && secondValue < resultsCache[firstValue].count ?
resultsCache[firstValue][secondValue]:firstValue + secondValue
//等价
if firstValue < resultsCache.count && secondValue < resultsCache[firstValue].count {
return resultsCache[firstValue][secondValue]
}else{
return firstValue + secondValue
}
}
}
}
var calc1 = Sum(first:0, second: 9).Result
var calc2 = Sum(first:3, second: 8).Result
print("Calc1: \(calc1) Calc2: \(calc2)");
//9 , 12
我定义了一个名为类总和,产生传递到其初始值设定项的两个整数值的总和。作为一种优化的初始值设定项笔类创建一个二维 Int 数组,以交易时间在晚些时候对快的运算速度初始化过程中预先计算好的值填充。
有定义总和类,我然后创建两个实例,并使用它们来执行计算。我创建一个新的每次总和对象,我招致创建和填充二维阵列的成本 — — 可以被测量在存储计算的值所需的内存和计算成本。我完成的两个运算结果写入控制台,产生下面的输出 ︰
Calc1: 9 Calc2: 11
这可能看起来像一个不切实际的例子,但这种编码风格是很常见,通常是过早的优化,其中一个程序员试图推测提高代码的性能,因为它被写,而不是由于后续的性能测试的结果 — — 的东西,通常会导致糟糕的性能和更少的可读的代码。然而,这也是在此示例中是不现实的两个方面。第一个是工作由总和类是如此简单,即使是最狂热的优化器是不太可能看到添加两个整数作为值得缓存的成本。第二个方面是playground上显示总和类和创建实例从它在同一个文件的两个语句。在实际项目中,初始化代码迷失在深的层次结构的类,并使用类的语句将在完全不同的应用程序部分
创建模板的依赖关系
从模板创建一个新对象,组件必须具备三条信息。
■ 这是与对象关联的模板
■ 初始值设定项,必须调用
■ 名称和类型初始值设定项参数
对象的一个新实例在哪里需要将此信息传播整个应用程序。这带来的问题是它在模板上,创建一个依赖项这样当模板更改时,所有使用该模板创建新对象的组件必须更新以反映该更改。你可以看到这在清单 5-2,在那里我已经修改总和类,它定义了一个额外的初始值设定项的参数。
class Sum {
var resultsCache: [[Int]]
var firstValue:Int
var secondValue:Int
init(first:Int, second:Int, cacheSize:Int) {
resultsCache = [[Int]](count: cacheSize, repeatedValue:[Int](count:cacheSize, repeatedValue: 0))
for i in 0 ..< cacheSize {
for j in 0 ..< cacheSize {
resultsCache[i][j] = i + j
}
}
self.firstValue = first
self.secondValue = second
}
var Result:Int {
get {
return firstValue < resultsCache.count
&& secondValue < resultsCache[firstValue].count
? resultsCache[firstValue][secondValue]
: firstValue + secondValue
}
}
}
var calc1 = Sum(first:0, second: 9, cacheSize:100).Result;
var calc2 = Sum(first:3, second: 8, cacheSize:20).Result
println("Calc1: \(calc1) Calc2: \(calc2)");
初始值设定项参数用于控制生成的缓存结果的数目。如清单所示,我不得不更新创建语句总和对象使用订正的初始值设定项。变更是平凡的当只有两个语句,创建总和对象是彼此相邻,但这些建设语句可以分布在实际项目中,和每一个需有足够的知识关于执行总和类来提供一个合理的值为 cacheSize 初始值设定项参数。
提示我在这一章的目标是展示原型模式,但还有其他的方法来解决这类问题。我可以例如,定义一个方便的初始值设定项,调用指定的初始值设定项和提供的默认值 cacheSize 参数。如第 1 章中,模式并不总是解决问题的唯一方法。
理解原型模式
原型模式使用一个现有的对象 — — 而不是类或结构 — — 以创建新的对象。这是通常指作为克隆,因为新对象是完全相同的副本,在现有的包括对已创建以来,它的对象的存储属性所做的任何更改。图 5-1 演示原型模式的工作原理。
有 3 个操作的原型模式。首先,需要对象的组件要求原始对象 (称为原型) 进行自我复制。第二次手术是在其中创建新的对象 (称为克隆) 的复制过程。在最后一个操作,原型给调用组件的克隆,完成复制过程。
实现原型模式
Swift 自动应用原型模式,当你到一个新的变量指派值类型。值类型定义使用结构,和所有类型的内置swift实现为幕后,意味着你可以只是通过将他们分配给新变量克隆字符串、 布尔值、 集合、 枚举、 元组和数值类型的结构。swift将复制原型的价值,并使用它来创建一个克隆。清单 5-3 所示的内容 ValueTypes.playground文件,我创建了演示如何克隆值类型.
struct Appointment {
var name:String
var day:String
var place:String
func printDetails(label:String) {
println("\(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")
原型模式依赖于那里正在 — — 显然不够 — — 原型对象,通常从模板创建。这似乎有悖常理,但你必须要能够以某种方式得到的原型。一旦我有原型,我将它分配给一个新的变量,这样创建副本 ︰
...
var workMeeting = beerMeeting;
...
在这一点上,我有两个单独的任命分配给对象beerMeeting和workMeeting变量。天的名称, ,和放置两个对象的属性具有相同的值,如图所示的5 2.
图 5-2。克隆一个结构原型的影响
一旦我创建克隆,我分配到新值名称, 一天,和地方配置克隆来代表不同的约会从一个由原型,如图所示的属性5-3.
图 5-3。配置克隆
我完成通过调用printDetails方法对两个任命对象,会产生以下结果 ︰
Social with Bob on Mon at Joe's Bar
Work with Alice on Fri at Conference Rm 2
一旦我创建的原型,我可以创建和配置尽可能多的克隆,因为我需要不会招致与使用结构模板相关的开销。
克隆的引用类型
使用类创建的对象是引用类型,和 Swift 不会复制这些对象,当你将它们分配给一个新的变量。相反,对对象的新引用创建这两个变量引用同一个对象。清单 5-4 显示的内容 ReferenceTypes.playground 文件,在其中我已经修改任命从前面的章节,将类用作模板而不是一个结构的例子。
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) { println("\(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 创建一个默认初始值设定项为结构而不是类。除了使用一个类来定义任命类型,本示例包含相同的代码清单 5-3。安慰,然而,所示的结果不同。
上周五在会议 Rm 2 的 Alice 与社会
与爱丽丝于星期五工作会议 Rm 2
这里的问题就是其中一个任命对象,和它指由两个workMeeting和beerMeeting变量,如图所示
因为只有一个任命对象、 发生变化,使到存储的属性通过workMeeting回来时我访问通过属性,读取变量beerMeeting变量,这就是为什么我得到意想不到的事情 — — 并且无益 — — 在控制台输出。
执行 NSCopying 议定书 》
分配新对象的引用现有是面向对象编程的重要组成部分,但它是没有帮助的原型模式。支持克隆, 基金会框架定义了 NSCopying 协议,该对话框允许您指定应如何克隆的对象。清单 5-5 显示了我如何更新任命类来实现 NSCopying 协议。
import Foundation
class Appointment : NSObject, NSCopying {
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)");
}
func copyWithZone(zone: NSZone) -> AnyObject {
return Appointment(name:self.name, day:self.day, place:self.place);
}
}
var beerMeeting = Appointment(name: "Bob", day: "Mon", place: "Joe's Bar");
var workMeeting = beerMeeting.copy() as Appointment;
workMeeting.name = "Alice";
workMeeting.day = "Fri";
workMeeting.place = "Conference Rm 2";
beerMeeting.printDetails("Social");
workMeeting.printDetails("Work");
提示可以实现NSCopying仅在类和非结构的协议。结构是总是主题
对浅层复制。
NSCopying协议定义了copyWithZone当复制该对象时调用的方法。
用来复制对象的机制留给类来实现,并在这种情况下,我创建的一个新实例任命类使用从当前对象的存储的属性值。
你可以忽略NSZone在论证时,执行copyWithZone方法。
利用NSCopying协议,我要改变任命那它从派生类类NSObject,它定义了副本方法。若要复制任命,我打电话给副本方法 — — 和不 copyWithZone— — 为原型,像这样 ︰
...
var workMeeting = beerMeeting.copy() as Appointment;
...
copyWithZone方法返回AnyObject,这意味着我有向下转换由创建的对象副本方法与作为关键字以便workMeeting变量键入正确。
警告执行NSCopying协议不会更改为引用类型转换为值类型。你必须调用副本克隆原型的方法。如果你只是将原型分配给一个新的变量,您将结束对原型并不是一个新的对象的新引用。
了解浅层和深层复制
原型模式的的一个重要方面是是否使用深复制或浅复制,它涉及如何存储引用的属性类型处理其他参考克隆对象。在清单 5-6,我定义了一类新的 ReferenceTypes playground和添加新属性设置为约会 ,是指它的实例的类
import Foundation
class Location {
var name:String;
var address:String;
init(name:String, address:String) {
self.name = name;
self.address = address;
}
}
class Appointment : NSObject, NSCopying {
var name:String;
var day:String;
var place:Location;
init(name:String, day:String, place:Location) {
self.name = name;
self.day = day;
self.place = place;
}
func printDetails(label:String) {
print("\(label) with \(name) on \(day) at \(place.name), " + "\(place.address)");
}
func copyWithZone(zone: NSZone) -> AnyObject {
return Appointment(name:self.name, day:self.day,place:self.place);
}
}
var beerMeeting = Appointment(name: "Bob", day: "Mon",
place: Location(name:"Joe's Bar", address: "123 Main St"))
var workMeeting = beerMeeting.copy() as Appointment;
workMeeting.name = "Alice";
workMeeting.day = "Fri";
workMeeting.place.name = "Conference Rm 2";
workMeeting.place.address = "Company HQ";
beerMeeting.printDetails("Social");
workMeeting.printDetails("Work");
我已经创建了一个简单的位置类并使用它为地方属性的任命对象。这里是显示在控制台中的输出 ︰
Social with Bob on Mon at Conference Rm 2, Company HQ
Work with Alice on Fri at Conference Rm 2, Company HQ
再一次的变化通过所workMeeting变量有受影响的存储的值可通过workMeeting变量。这是因为我改变的类型放置属性的值类型 (字符串) 为引用类型 (位置) 和我实现的 NSCopying 协议创建新引用的原型对象
这是称为浅表副本,在其中复制对象的引用,不是对象本身。如图 5-5 所示,有两个约会对象,但其放置属性引用同一个位置对象。这就是为什么我申请通过的变化 workMeeting.place 属性会影响我通过的值 beerMeeting.place 属性。
提示与NSCopying协议是@NSCopying属性特性,可以应用于存储属性。 @NSCopying 属性将自动调用 copyWithZone 方法的对象指派给的附加说明的属性,和我将演示其使用在本章后面的"示例的原型模式在可可粉"一节。
实施深度复制
深度复制每个创建的原型,而在这种情况下将确保所提到的所有对象的副本任命对象是指不同位置对象通过其地方属性。清单 5-7 显示了如何实现深度复制在 ReferenceTypes playground。
import Foundation
class Location : NSObject, NSCopying {
var name:String;
var address:String;
init(name:String, address:String) {
self.name = name;
self.address = address;
}
func copyWithZone(zone: NSZone) -> AnyObject {
return Location(name: self.name, address:self.address);
}
}
class Appointment : NSObject, NSCopying {
var name:String;
var day:String;
var place:Location;
init(name:String, day:String, place:Location) {
self.name = name;
self.day = day;
self.place = place;
}
func printDetails(label:String) {
print("\(label) with \(name) on \(day) at \(place.name), "+ "\(place.address)");
}
func copyWithZone(zone: NSZone) -> AnyObject {
return Appointment(name:self.name, day:self.day,place:self.place.copy() as Location);
}
}
var beerMeeting = Appointment(name: "Bob", day: "Mon",
place: Location(name:"Joe's Bar", address: "123 Main St"));
var workMeeting = beerMeeting.copy() as Appointment;
workMeeting.name = "Alice";
workMeeting.day = "Fri";
workMeeting.place.name = "Conference Rm 2";
workMeeting.place.address = "Company HQ";
beerMeeting.printDetails("Social");
workMeeting.printDetails("Work");
若要创建深层副本,必须执行NSCopying议定书 》位置类,改变该基类NSObject,并定义copyWithZone方法。你要深拷贝的所有引用类型必须都实现 NSCopying 你的原型,包括那些通过其他提及所提到的协议,所以您必须重复此过程在整个类引用。
选择浅或深复制
有没有什么一成不变的规则之间浅层和深层复制,选择和决定,便在类的类的基础上作出。你应该考虑三个因素 ︰ 所需复制的对象的存储复制和复制的对象将被使用的方式所需的内存量的工作量。
它是最后一个因素 — — 将如何使用复制的对象 — — 那就是最重要。情况下预约对象毫无任何意义,因为对某个约会的位置所做的更改不太可能将应用于其他约会,引用同一个位置的位置在清单 5-7,之间共享对象类对象,尤其是当社会和工作任用混合在一起。
它很可能是一组相关的约会,将受益于一个共享位置。想象一下,作为一个例子,一整天都在同一会议室举行的会议的系列 — — 您的应用程序可能会受益于优化创造的位置对象的相关会议。在这种情况下,您应该平衡计算和创建并保存一个新的对象,针对管理对共享对象的引用的复杂性所需内存的量。在任命/位置示例中, 位置对象很容易创建和需要这种很少的存储 (只是两个字符串值) 的开销和复杂性的工作什么时候位置可以共享对象和当他们不能只是不合理的。
我能给的最好建议是想通过对象和图的目的出哪些拟共同跨越所有副本创建从原型。如果你不确定,然后开始与浅复制,因为它是最简单的执行 — — 它不会总是正确的技术,但它允许您测试变动的影响,而不必执行 NSCopying 在您的应用程序协议。
由于前面提到的实施NSCopying协议不会改变引用类型转换为值类型,所以我必须调用副本方法来创建一个克隆任命原型的位置对象,我在做copyWithZone所定义的方法任命班上
func copyWithZone(zone: NSZone) -> AnyObject {
return Appointment(name:self.name, day:self.day,place:self.place.copy() as Location);
}
通过查看控制台输出从这里所示的示例代码,您可以看到深拷贝的影响 ︰
Social with Bob on Mon at Joe's Bar, 123 Main St
Work with Alice on Fri at Conference Rm 2, Company HQ
由于任命对象有自己位置对象、 发生变化,可通过workMeeting变量有没有通过访问对象上的效果beerMeeting变量。
复制阵列
swift数组是作为结构,这使得它们值类型实现的。当你将一个数组分配给一个新的变量时,该数组本身被复制以及它包含任何值类型。数组中包含的引用类型是浅层复制,这样原型数组和克隆数组都将包含对同一对象的引用。清单 5-8 所示的内容 ArrayCopy.playground 文件,造成提供示范。
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 people = [Person(name:"Joe", country:"France"), Person(name:"Bob", country:"USA")];
var otherpeople = people;
people[0].country = "UK";
print("Country: \(otherpeople[0].country)");
提示作为一种性能优化,swift阵列不复制直到你修改它们,称为懒的复制。这不是你需要担心在日常的基础上,因为它无缝地发生在幕后,但它意味着如果你只在读该数组的内容,像一次值类型引用类型进行修改像克隆的数组。
我创建的数组称为人包含两个人对象。我将数组赋给一个变量称为别人 ,然后再修改中的第一个对象人数组。这里是控制台的输出,显示出该数组的内容确实浅复制,即使阵列本身是一个结构 ︰
Country: UK
深深地复制数组,必须检查每个数组中的项和查找的对象的类从派生NSObject和实现NSCopying协议,如清单 5-9 所示。
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);
}
}
func deepCopy(data:[AnyObject]) -> [AnyObject] {
return data.map({item -> AnyObject in
if (item is NSCopying && item is NSObject) {
return (item as NSObject).copy();
} else {
return item;
}
})
}
var people = [Person(name:"Joe", country:"France"), Person(name:"Bob", country:"USA")];
var otherpeople = deepCopy(people) as [Person];
people[0].country = "UK";
print("Country: \(otherpeople[0].country)");
提示在"示例的原型模式在可可粉"部分中,我解释如何复制可可阵列,由执行NSArray和NSMutableArray类。这些类以不同的方式对待我在本节中描述的内置swift阵列和了解他们的工作可以是有用的使用目标 C 代码时。
我定义了一个称为函数deepCopy接受一个数组并使用地图方法将复制数组。我递给关闭地图方法检查是否该对象可以是 deepcopied,如果它可以调用副本方法。其他对象添加到结果数组而无需修改。此控制台输出所示,深深地复制的阵列不再包含对同一对象的引用 ︰
Country: France
了解原型模式的好处
在以下章节中,我描述了原型模式提供的好处。其中一些解决章开头有关的问题却从对象复制通过的方式产生的额外好处 NSCopying 协议。
避免昂贵的初始化
使用NSCopying协议允许对象采取负责复制自己,这意味着克隆可以避免昂贵的初始化操作。在这一章的开始,我用 Initialization.playground 文件来定义总和在其初始值设定项,我在其中已重复清单 5-10 生成缓存结果的数组类。