第14章:原型模式

原型模式是一种创造模式,允许一个对象复制自己。它涉及两种类型。

  1. 一个声明复制方法的复制协议。

  2. 一个符合复制协议的原型类。

实际上有两种不同类型的拷贝:浅层和深层。

浅层拷贝创建了一个新的对象实例,但并不复制其属性。任何引用类型的属性仍然指向相同的原始对象。例如,每当你复制一个Swift数组(这是一个结构,因此在赋值时自动发生),就会创建一个新的数组实例,但其元素不会被复制。

深度复制会创建一个新的对象实例,同时也会复制每个属性。例如,如果你深度复制一个数组,它的每个元素也会被复制。Swift默认没有为数组提供一个深度复制方法,所以你将在本章中创建一个方法

你应该在什么时候使用它?

使用这种模式可以使一个对象能够复制自己。

例如,Foundation定义了NSCopying协议。然而,这个协议是为Objective-C设计的,不幸的是,它在Swift中并不那么好用。你仍然可以使用它,但你会自己写更多的模板代码。

相反,你将在本章中实现你自己的复制协议。你会通过这种方式深入了解原型模式,而且你的实现也会更符合Swifty的要求!

Playground实例

打开Starter目录下的IntermediateDesignPatterns.xcworkspace,然后打开Prototype页面。

对于这个例子,你将创建一个复制协议和一个符合该协议的怪物类。在代码示例后添加以下内容。

  1. 你首先声明一个必要的初始化器,init(_ prototype: Self)。这被称为复制初始化器,因为它的目的是用一个现有的实例来创建一个新的类实例。

  2. 2.你通常不会直接调用复制初始化器。相反,你只需在你想复制的符合条件的复制类实例上调用copy()。

由于你在协议本身中声明了复制初始化器,所以copy()是非常简单的。它通过调用type(of: self)来确定当前的类型,然后调用拷贝初始化器,传入self实例。因此,即使你创建了一个符合Copying的类型的子类,copy()也会正常工作。

接下来,请看下面的代码。

以下是该代码的作用。

  1. 这声明了一个简单的怪物类型,它符合复制的要求,并有健康和等级的属性。

  2. 为了满足复制的要求,你必须声明init(_ prototype:)。

然而,你可以将其标记为方便,并调用另一个指定的初始化器,这正是你所做的。

接下来,添加以下代码。

对上述代码进行逐条评论。

  1. 在一个真正的应用程序中,你可能也会有怪物的子类,这将增加额外的属性和功能。在这里,你声明了一个EyeballMonster,它增加了一个可怕的新属性--红度。哦,它是如此的红,如此的恶心! 不要碰那个眼球!

  2. 由于你添加了一个新的属性,你还需要在初始化时设置其值。为了做到这一点,你创建了一个新的指定初始化器:init(health:level:redness:)

  3. 由于你创建了一个新的初始化器,你还必须提供所有其他需要的初始化器。请注意,你需要用一般的类型Monster来实现,然后把它投给EyeballMonster。这是因为特化为EyeballMonster将意味着它不能接受Monster的另一个子类,这将破坏这是对Monster所要求的初始化器的覆盖这一条件。

现在,你已经准备好试用这些类了! 添加以下内容。

let monster = Monster(health: 700, level: 37) 
let monster2 = monster.copy() 
print("Watch out! That monster's level is \(monster2.level)!")

在这里,你创建了一个新的怪物,创建了一个名为 monster2 的副本,然后打印 monster2.level。你应该在控制台中看到这个输出。

Watch out! That monster's level is 37!

接下来输入以下内容。

你在这里证明,你确实可以创建EyeBallMonster的副本。你应该在控制台中看到这个输出。

Eww! Its eyeball redness is 999!

如果你试图从一个怪物创建一个EyeballMonster,会发生什么?输入下面的最后一句话。

let eyeballMonster3 = EyeballMonster(monster)

这个编译结果很好,但会引起运行时异常。这是因为你在前面进行了强制投递,你把prototype称为! EyeballMonster。

注释掉这一行,这样playground就可以重新运行了。

理想情况下,你不应该允许在怪物的任何子类上调用init(_ monster:)。相反,你应该总是调用copy()。

你可以通过将子类方法标记为 "不可用 "来向其他开发者表明这一点。在子类的 init(_ monster:) 前面添加以下一行。

@available(*, unavailable, message: "Call copy() instead")

然后,取消对eyeballMonster3这一行的注释,你将在playground控制台得到这个错误信息。

error: 'init' is unavailable: Call copy() instead

很好,这可以防止直接调用这个方法 继续,再次注释掉这一行,这样playground就可以运行了。

你应该注意什么?

如playground的例子所示,默认情况下,可以将超类实例传递给子类的复制初始化器。如果一个子类可以从超类实例中完全初始化,这可能不是一个问题。然而,如果子类添加了任何新的属性,可能就无法从超类实例中初始化它。

为了缓解这个问题,你可以把子类的拷贝初始化器标记为 "不可用"。作为回应,编译器将拒绝编译对这个方法的任何直接调用。

仍然有可能间接地调用这个方法,就像copy()那样。然而,对于大多数使用情况来说,这种保护措施应该是 "足够好 "的。

如果这不能防止你的用例出现问题,你就需要考虑你到底要如何处理它。例如,你可以向控制台打印一条错误信息,然后崩溃,或者你可以通过提供默认值来处理它。

教程项目

在接下来的几章中,你将完成一个名为MirrorPad的应用程序。这是一个绘图应用程序,允许用户创建动画的镜像图。

在Starter目录下,在Xcode中打开MirrorPad\MirrorPad.xcodeproj。

构建并运行以尝试该应用程序。用你在真实设备上的手指或模拟器上的鼠标在左上方的视图中作画。

然后按Animate键,你的画就会在屏幕上被重新绘制成动画。超级酷

然而,该应用程序应该将图像复制并反射到其他每个视图中。这一点目前还没有实现,因为该应用程序不知道如何复制任何东西!你的工作就是把它复制到其他视图中。你的工作就是解决这个问题。

打开DrawView.swift,看看这个类。这是应用程序的核心:当touchesBegan被调用时,它创建一个新的LineShape对象,当touchesMoved被调用时,它向LineShape添加点。

接下来,打开LineShape.swift,看看这个类。这是CAShapeLayer的一个子类(见https://developer.apple.com/documentation/quartzcore/cashapelayer)。 它用于从路径中创建简单、轻量级的形状层。如果LineShape是可复制的,你就可以把它们每个都复制到屏幕上的其他DrawView实例中。

然而,首先,你需要确定 "可复制 "的实际含义。

在文件层次结构中的协议组下,创建一个名为Copying.swift的新Swift文件,并将其内容改为以下内容。

  1. 你首先声明一个新的复制协议,它与playground例子中的协议完全相同。

  2. 然后,当Array的元素符合Copying协议时,你在Array上创建一个扩展。

在那里,你创建了一个新的方法,叫做deepCopy(),它使用map来创建一个新的数组,其中每个元素都是通过调用copy()生成的。

回到LineShape.swift,将类的声明替换为以下内容。

public class LineShape: CAShapeLayer, Copying {

然后,将init(layer: Any)替换为以下内容。

public override convenience init(layer: Any) { 
  let lineShape = layer as! LineShape 
  self.init(lineShape) 
}

public required init(_ prototype: LineShape) {
  bezierPath = prototype.bezierPath.copy() as! UIBezierPath 
  super.init(layer: prototype)

  fillColor = nil 
  lineWidth = prototype.lineWidth    
  path = bezierPath.cgPath 
  strokeColor = prototype.strokeColor
}

init(layer:)看起来和init(_ prototype:)非常熟悉。这个方法是Core Animation内部在做图层动画时使用的。然而,为了真正符合复制的要求,方法的签名必须与init(:)完全匹配。因此,你只需将init(layer:)移交给init(:),就可以满足核心动画和复制的要求了。

你还需要一个方法来实际复制每个LineShape到DrawView上。打开DrawView.swift,在类的结尾大括号前添加以下内容。

public func copyLines(from source: DrawView) { 
  layer.sublayers?.removeAll() 
  lines = source.lines.deepCopy() 
  lines.forEach { layer.addSublayer($0) } 
}

这个方法首先删除了所有的子层,这些子层代表了现有的LineShape层。然后,它从作为源传递的DrawView中创建一个deepCopy。最后,它将每条线添加到该层。

最后,你实际上需要在屏幕上的Animate按钮被按下时调用这个方法。打开ViewController.swift,在animatePressed(_:)的大括号后添加以下内容。

mirrorDrawViews.forEach { $0.copyLines(from: inputDrawView) } 
mirrorDrawViews.forEach { $0.animate() }

这首先迭代了每个mirrorDrawView并复制了输入DrawView。然后在每个mirrorDrawView上调用animate()来启动动画。

建立并运行,在左上角的输入视图中绘图,然后按Animate。


上一章 目录 下一章