学院派和现实派的交锋,如何改进 Delegate Pattern

在公司项目组里有一个上了年纪的前辈开发者,他从遥远的 Carbon 时代就开始做 Apple 平台的开发。最近在项目里合作,写了很多 delegate 给他用,顺便和他讨论了一下经典的 protocol-delegate 这套东西,发现其实我们可以有一些“改良”和简化的余地。

我们从故事的开始说起:

Cocoa 中的 Delegate Pattern

委托模式 (delegate pattern) 在 Cocoa 中实在是太常见了,它作为 Apple 平台开发的一个基础模式,被大量使用在 Cocoa API 中。而第三方开发者也自然而然地遵循了类似的模式,我们对下面这样的代码应该非常熟悉了:

protocol TextInputViewDelegate: class {
    func textInputView(_ view: TextInputView, didConfirmInput text: String?)
}

class TextInputView: UIView {

    @IBOutlet weak var inputTextField: UITextField!
    weak var delegate: TextInputViewDelegate?

    @IBAction func confirmButtonPressed(_ sender: Any) {
        delegate?.textInputView(self, didConfirmInput: inputTextField.text)
    }
}

TextInputView 负责通过一个 UITextField 控件收集用户输入,然后在用户点击确认按钮时,将控件中字符串的结果通过 delegate 的方法传递给负责实际处理的对象 (一般是某个 View Controller)。

注意,我们将 delegate 标记为了 weak,这防止了引用循环造成的内存泄漏。我假设您已经完全理解这里添加 weak 标记的原因,如果这个假设不成立的话,您可以先阅读一下我的另外一篇关于 weak 引用的文章

使用侧,只需要声明遵守 TextInputViewDelegate 并实现相关方法:

class ViewController: UIViewController {

    @IBOutlet weak var textLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        let inputView = TextInputView(frame: /*...*/)
        inputView.delegate = self
        view.addSubview(inputView)
    }
}

extension ViewController: TextInputViewDelegate {
    func textInputView(_ view: TextInputView, didConfirmInput text: String?) {
        textLabel.text = text
    }
}

一切都很美好,代码正常工作。不过,这种常用模式在 Swift 的世界中已经显得有些跟不上时代了。主要问题是,我们需要创建很多额外的模板代码,像是整个 TextInputViewDelegate,像是 TextInputView 中的 delegate。另外,在 ViewController 中,将 self 设置为 inputView.delegate 的代码和实际进行代理方法处理的 textInputView(_:didComfirmInput:) 方法无法写在一起,在代码行数比较多的时候,这往往还会造成维护和定位的麻烦。

这种写法是从 Objective-C 1.0 的时代继承下来的,也是很多 iOS 开发者入门时必学的模式。在 Cocoa 框架中也由于历史原因,大量存在这种“声明 protocol”,“调用 delegate 方法”,“遵守和处理 delegate” 的三部曲实现,可谓是学院派的写法。

Modern Swift 的新选项

如果你写过 Objective-C 的话,相信你一定对 block 的各种声明方式了如指掌,比如在方法参数中如何声明,在类的成员中又如何声明。因为 Objective-C 的 block 是如此简单,所以大家都很喜欢使用它。

嗯...好吧,对不起,我已经编不下去了。我自己是完全无法准确记住如何在 Objective-C 里声明一个 block 的,我要么依赖自动补全的提示,要么就只好求助这个网站。如果你能准确背出所有声明方式的话,只能说...你是不是刚准备完一场面试啊...

也正因为 Objective-C 的 block 声明如此反人类,所以一般开发者很少会在除了回调需要的场合去直接使用它。但是 Swift 中情况却大大不同。只要你写过 Swift,相信你一定对 closure 的各种声明方式了如指掌,比如在方法参数中如何声明,在类的成员中又如何声明。因为它们都是统一的,只需要用 (Input) -> Output 的方式声明就可以了!Swift 的 closure 是如此简单,所以大家都很喜欢使用它。相比起上面的委托代理的形式,下面这种写法在 Swift 中非常流行。

class TextInputView: UIView {

    @IBOutlet weak var inputTextField: UITextField!
    var onConfirmInput: ((String?) -> Void)?

    @IBAction func confirmButtonPressed(_ sender: Any) {
        onConfirmInput?(inputTextField.text)
    }
}

使用侧,不再需要声明遵守 protocol 也不用去实现对应方法了,直接设置 onConfirmInput 即可:

class ViewController: UIViewController {

    @IBOutlet weak var textLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        let inputView = TextInputView(frame: /*...*/)
        inputView.onConfirmInput = { text in 
            self.textLabel.text = text
        }
        view.addSubview(inputView)
    }
}

这节省了很多的代码,清晰地将对于回调的处理写在了更合适的地方。不过,稍有经验的开发者会很快看到,这么做有一个致命的错误,内存泄漏!在 onConfirmInput 中,我们让 inputView 持有了 self,而同时 self 通过 subviews 强引用着 inputView,这两个对象之间形成了引用循环,我们无法释放它们了!

当然,解决方法也很简单,我们只需要在设置 onConfirmInput 的时候使用 [weak self] 来将闭包中的 self 换为弱引用即可:

inputView.onConfirmInput = { [weak self] text in
    self?.textLabel.text = text
}

这为使用 onConfirmInput 加上了一个前提:你大概率需要将 self 标记为 weak 以避免犯错,否则你将写出一个内存泄漏。这个泄漏无法在编译期间定位,运行时也不会有任何警告或者错误,这类问题也极易带到最终产品中。在开发界有一句话是真理:

如果一个问题可能发生,那么它必然会发生。

你不可能期望 onConfirmInput 的使用者时刻牢记给 self 加上 weak。和经典的 delegate 模式一样,我们最好应该在声明 delegate 的时候在其中就将 self 指定为 weak,这样就能从根源上一次性避免所有这类错误。

top Created with Sketch.