Skip to content

原型模式 Prototype Pattern

pro648 edited this page Aug 15, 2019 · 1 revision

原型模式(Prototype Pattern)属于创建型模式,Prototype 允许我们远离从 client 创建新实例的复杂操作,而采用复制已有对象,被复制的实例就是我们所称的原型。由于从零创建对象可能包含昂贵的操作,复制对象可以节省资源和时间。在需要时可以修改新复制对象的属性。

Prototype pattern 包含以下两部分:

PrototypePattern

  1. Copying:该协议声明了copy()方法。
  2. Prototype:遵守Copying协议的 class。

复制分为浅复制(shallow copy)、深复制(deep copy)。

Shallow copy 创建对象时不复制其属性,任何引用类型的属性仍指向原始属性。例如,Swift 的Array是 struct 类型,当赋值或复制时,只创建新数组对象,并不会复制数组元素。

Deep copy 创建对象时会复制其所有属性。例如,深复制Array时,会同时复制其所有元素。Swift 默认不提供数组的深复制。这篇文章将涉及浅复制、深复制。

如果数组的元素仍然是数组,深复制只会复制一层,并不会把里层数组的元素进行复制。想要了解更多,可以查看我的另一篇文章:深复制、浅复制、copy、mutableCopy

何时使用 Prototype Pattern

Prototype pattern 能够使对象自我复制。

原型模式多用于创建复杂的或者耗时的实例。因为这种情况下,复制一个已经存在的实例使程序运行更高效,或者创建值相等,只是名称不一样的同类数据。

Foundation框架定义了NSCopying协议,但该协议是为 Objective-C 设计的,在 Swift 中效果不佳。虽然仍然可以使用NSCopying协议,但需要编写样板代码(boilerplate code)。

在这篇文章中将实现自己Copying协议,以此深入学习 prototype。

示例

这篇文章中将会创建Copying协议、Monster类,Monster类遵守Copying协议。创建Copying.swift文件并添加以下代码:

import Foundation

public protocol Copying: class {
    // 声明 required initializer
    init(_ prototype: Self)
}

extension Copying {
    // 通常不直接调用初始化器,而是调用 copy() 方法
    public func copy() -> Self {
        return type(of: self).init(self)
    }
}

首先,声明 required initializer 方法。由于该方法是利用已有实例创建新的实例,因此该方法也被称为 copy initializer。

其次,要复制已遵守Copying协议的实例时,通常不直接调用初始化器,而是调用copy()方法。因为已经在Copying协议内实现了 copy initializer,copy()方法将变得非常简单。通过type(of: self)方法获取当前对象类型,之后调用 copy initializer,并为 copy initializer 传入当前实例。因此,即使创建了遵守Copying协议类的子类,copy()方法也能正常运行。

创建Monster.swift文件,并添加以下代码:

import Foundation

public class Monster: Copying {
    public var health: Int
    public var level: Int
    
    public init(health: Int, level: Int) {
        self.health = health
        self.level = level
    }
    
    public required convenience init(_ monster: Monster) {
        self.init(health: monster.health, level: monster.level)
    }
}

为了遵守Copying协议,必须将init(_ prototype:)声明为 required。但是,同时可以将 designated initializer 标记为convenience

如果想要对数组实现深复制,可以使其元素遵守Copying协议,如下:

extension Array where Element: Copying {
    public func deepCopy() -> [Element] {
        return map { $0.copy() }
    }
}

这样即可实现数组深复制。

创建EyeballMonster.swift文件,并添加以下代码:

import Foundation

// EyeballMonster 为 Monster 子类,同时添加了 redness 属性。
public class EyeballMonster: Monster {
    public var redness = 0
    
    // 在初始化方法为新增加的属性赋值
    public init(health: Int, level: Int, redness: Int) {
        self.redness = redness
        super.init(health: health, level: level)
    }
    
    public required convenience init(_ prototype: Monster) {
        let eyeballMonster = prototype as! EyeballMonster
        self.init(health: eyeballMonster.health,
                  level: eyeballMonster.level,
                  redness: eyeballMonster.redness)
    }
}

除实现init(health:level:redness)方法外,还需实现其他 required initializer。该 required initializer 行参为 Monster 类型,随后强转为 EyeballMonster。如果行参为 EyeballMonster 类型,则行参不能为 Monster 的其他子类。

ViewController.swift文件中ViewDidLoad方法内添加以下代码:

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // 创建 Monster 的实例,复制后得到 monster2
        let monster = Monster(health: 700, level: 37)
        let monster2 = monster.copy()
        print("Watch out! That monster's level is \(monster2.level)")
    }

控制台输出如下:

Watch out! That monster's level is 37

继续添加以下代码:

        // 创建 EyeballMonster 的实例,复制后得到 eyeball2
        let eyeball = EyeballMonster(health: 3002, level: 60, redness: 648)
        let eyeball2 = eyeball.copy()
        print("Eww! Its eyeball redness is \(eyeball2.redness)")

这里可以验证是否可以复制 EyeballMonster 对象,控制台输出如下:

Eww! Its eyeball redness is 648

如果想要从 Monster 创建 EyeballMonster 对象呢?添加以下代码:

        let eyeballMonster3 = EyeballMonster(monster)

此时,编译器没有报错。但在运行时会抛出异常。这是由于 prototype as! EyeballMonster 的强转导致。

注释掉该行强转代码后,编译器会提示eyeballMonster变量没有redness属性。事实上,应该禁止 Monster 的子类调用init(_ monster:)方法,而应调用copy()方法。

可以通过将init(_ monster:)方法标记为不可用,避免调用错误。

    @available (*, unavailable, message: "Call copy() instead")
    public required convenience init(_ prototype: Monster) {
        ...
    }

此时,编译器会在 eyeballMonster3 处出现以下提示:

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

如上面示例所示,默认情况下,可以把超类实例传递给子类的 copy initializer。如果可以从超类实例完全初始化子类,则不会出现问题。如果子类添加了任何新属性,则无法从超类实例初始化。

为解决该问题,可以将子类 copy initializer 标记为 unavailable。此时,如果直接调用init(_:)方法,编译器会发出警告。虽然此时仍然能调用copy(),但通过将子类 copy initializer 标记为 unavailable 已经能够解决大部分情况。

如果此时仍然不能解决你的问题,就需要采取额外措施。例如,向控制台输出错误信息并抛出异常,为子类新添加的属性设置默认值等。

总结

以下是 Prototype Pattern 的关键点:

  • Prototype pattern 允许对象复制自身,其包括两部分:Copying协议和原型(prototype)。
  • Copying协议声明复制的方法,prototype 遵守Copying协议。
  • Foundation框架提供NSCopying协议,但该协议在 Swift 中表现不佳。你可以轻松推出自己的Copying协议,而无需依赖Foundation或任何框架。
  • 创建Copying协议的关键在于创建 copy initializer,并实现init(_ prototype:)方法。

Demo名称:PrototypePattern
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/PrototypePattern

参考资料:

  1. Prototype pattern in Swift
  2. Prototype Design Pattern
Clone this wiki locally