zoukankan      html  css  js  c++  java
  • Swift 实现观察者模式

    本文翻译自:An Observable Pattern Implementation in Swift

    问题

    在过去的几天里,我都在进行着 Gumroad's Small Product Lab 的挑战,就是使用Swift语言来开发一个Mac 应用。这个应用包含一个简单的 结构体 struct 类型 AppConfig, 表示应用中用户可以配置的选项。我所需要的就是创建一个ViewController 让用户可以编辑这些配置,也就是在这里我遇到了困难。

    在Objective-C 中通常的做法是 使用 Cocoa Bindings来实现。这是一个构建在KVO之上的好功能,让你能够在Interface Builder 上自动地绑定你的UI元素到实例变量。

    但是Cocoa Bindings 只作用于 NSObject 的子类上,我也想过直接把AppConfig 类型改为 NSObject,但把AppConfig作为一个 值类型是最佳的实践,而且我也不想因为这个小功能导入Objective-C的运行时。

    由于我本来就是要用Swift来进行挑战这个项目的,所以我决定自己实现一个Swift版本的观察者模式。

    到处都是协议

    首先我需要定义一个协议,封装了我的绑定操作:

    protocol ObservableProtocol {
        typealias T
        var value: T { get set }
        func subscribe(observer: AnyObject,
                   block: (newValue: T, oldValue: T) -> ())
        func unsubscribe(observer: AnyObject)
    }
    

    假设我们在一个对象类型的上下文中,这里是self,并实现了我们的协议,那么最终需要下面这样做:

    let initial = 3
    var v = initial
    var obs = Observable(initial)
    
    obs.subscribe(self) { (newValue, oldValue) in
        print("Object updated!")
        v = newValue
    }
    
    obs.value = 4  // Trigger update.
    print(v)       // 4!
    

    完美,现在开始写......

    实现

    考虑到观测对象有状态/生命周期,我决定把它作为类来对待:

    public final class Observable<T>: ObservableProtocol {
        // ...
    }
    

    我们将开始定义一个变量以及其它方便使用的类型:

    我们的subscribers 模型就是一个个的观察者,这是一个 ObserversEntry 元祖类型的数组,包括了一个监听对象和一个闭包,这个闭包会在观察的对象触发时执行。我们可以通过 unsubscribe 方法来查找添加的观察者,并移除特定的观察者。

    typealias ObserverBlock = (newValue: T, oldValue: T) -> ()
    typealias ObserversEntry = (observer: AnyObject, block: ObserverBlock)
    private var observers: Array<ObserversEntry>
    

    现在需要在我们的类中实现init方法,默认的构造器只需要一个简单的初始值,作为我们观察到的初始值。同时这个构造器也需要初始化我们的非可选的 observers 数组变量。

    init(_ value: T) {
        self.value = value
        observers = []
    }
    

    同时我们还要实现 didSet 方法,来在这个观察值发生变化时通知我们的观察者:

    var value: T {
        didSet {
            observers.forEach { (entry: ObserversEntry) in
                // oldValue is an implicit parameter to didSet in Swift!
                let (_, block) = entry
                block(newValue: value, oldValue: oldValue)
            }
        }
    }
    

    最后,我们需要实现 subscribe unsubscribe 方法来添加和删除我们的观察者:

    func subscribe(observer: AnyObject, block: ObserverBlock) {
        let entry: ObserversEntry = (observer: observer, block: block)
        observers.append(entry)
    }
    
    func unsubscribe(observer: AnyObject) {
        let filtered = observers.filter { entry in
            let (owner, _) = entry
            return owner !== observer
        }
    
        observers = filtered
    }
    

    注意:上面的实现只是最简单的,并没有考虑其它的异常情况。

    语法糖

    虽然上面的已经可以正常工作了,但是我还是想通过一些语法糖来减少重复编写 foo.value = <value>,所以我决定重载<< 这个符号。

    func <<<T>(observable: Observable<T>, value: T) {
        observable.value = value
    }
    

    更新: Chris Lattner ,Swift的发明者说了,建议我不要随意重载已经存在的操作符号,所以如果你使用这个代码,你可以重载 <~ 这个符号,或者其它相似的但是唯一的符号。

    例子

    /// A view controller supporting editing of the app's config.
    class PreferencesViewController: NSViewController {
        // The model layer.
        var configuration: ApplicationConfiguration
    
        // The view and object supporting the "controller".
        @IBOutlet var portTextField: NSTextField!
        var port: Observable<Int>
    
        // ...
    
        // MARK: NSViewController
        override func viewDidLoad() {
            super.viewDidLoad()
    
            port.subscribe(self) { (port, _) in
                // Ignore the old value, but update config with the new.
                self.configuration.port = port
    
                // You can trigger anything from here! Save to disk, etc...
                // Keeps action/UI code clean.
            }
    
            // ...
            // Assume `portTextFieldDidUpdate` is wired to be
            // called when portTextField's value updates.
            // ...
        }
    
        // MARK: Helpers
        func portTextFieldDidUpdate(value: Int) {
            port << value
        }
    }
    

    下载工程代码

  • 相关阅读:
    出现java.lang.NoClassDefFoundError: org/apache/commons/collections/FastHashMap错误问题解决
    选择一个更合适的编程语言
    23.if结构简单应用
    java环境的配置-传送门
    Java课程继续更新说明
    go语言熟知的开源项目
    go语言关于值类型和引用类型
    go语言实现生产者-消费者
    http协议——无连接、无状态
    jenkins结合gitlab实现提交代码自动构建
  • 原文地址:https://www.cnblogs.com/YungMing/p/5094724.html
Copyright © 2011-2022 走看看