[Swift翻译]Swift中混合器的力量

232 阅读9分钟

本文由 简悦SimpRead 转码,原文地址 jobandtalent.engineering

简介

正如任何新的现代语言所预期的那样,Swift 1.0推出了 协议 的概念。

协议作为一种方式来定义其他类型必须履行的方法、属性和契约的蓝图。我们从使用协议中得到的一些关键好处是。

  • 子类型/内涵多态性的使用。
  • 代码变更的容忍度。
  • 与I/O或其他不稳定的依赖关系如网络或持久性脱钩。

但这并不新鲜。我们在以前的Objective-C时代就已经习惯了这一切。

Swift 2.0改变了一切。协议现在可以有默认的实现,这为我们打开了一个全新的可能性世界。我们开始把一些功能捆绑在独立的协议中,这些协议的默认实现定义了相应的行为。然后,对象只需要采用该协议就可以获得所有适当的行为,而不需要从他们那边做额外的工作。这就是我们通常所说的 trait

特质的主要好处之一是,它们避免了继承的需要,而继承通常是为了获得某些功能而被_滥用的。此外,它们还允许_模仿_多种继承能力,但是和以往一样,当我们需要处理......状态时,我们就会遇到问题。

混合体是特质和状态的组合。我们可以说mixin是一个具有超能力的trait。

而......强大的力量伴随着巨大的责任。

一个更好的关联对象API

在Swift中,mixin在设计上是不允许的,因为协议扩展不能包含存储的属性,而只能是计算的属性。这对某些特定的行为来说是个很大的限制。迫使我们在符合协议的不同类中处理一些状态,使得解决方案相当繁琐,而且比一开始就使用纯混搭来的更加啰嗦。

然而,并不是所有的问题都解决了。我们要让Objective-C时代的一个不起眼的老朋友重获新生。关联对象。

关联对象是Objective-C运行时的一项功能,允许我们为现有的Swift类添加自定义属性。在正确的情况下,这是一个强大的便捷方式,不仅可以为我们自己的类添加状态,而且更有趣的是,可以为外部的类添加状态。一如既往,在错误的地方,我们可能会让一切都爆炸了。

关联对象可以追溯到Objective-C 2.0运行时,在2009年与OSX Snow Leopard一起推出。

我们可以通过使用三个简单的C方法来利用它的所有功能。

objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects

即使是简单的,这个API也远没有达到对Swift友好的程度。我们希望mixins尽可能容易使用和创建,所以用一个更现代、更安全的Swift API来包装这个老旧的C API是个好主意。

protocol AssociatedObjects: class {
    func associatedObject<T>(for key: UnsafeRawPointer) -> T?
    func setAssociatedObject<T>(
        _ object: T,
        for key: UnsafeRawPointer,
        policy: AssociationPolicy
    )
}

extension AssociatedObjects {
    func associatedObject<T>(for key: UnsafeRawPointer) -> T? {
        return objc_getAssociatedObject(self, key) as? T
    }
    
    func setAssociatedObject<T>(
        _ object: T,
        for key: UnsafeRawPointer,
        policy: AssociationPolicy = .strong
    ) {
        return objc_setAssociatedObject(
            self, 
            key, 
            object, 
            policy.objcPolicy
        )
    }
}

enum AssociationPolicy {
    case strong
    case copy
    case weak
    
    var objcPolicy: objc_AssociationPolicy {
        switch self {
        case .strong:
            return .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        case .copy:
            return .OBJC_ASSOCIATION_COPY_NONATOMIC
        case .weak:
            return .OBJC_ASSOCIATION_ASSIGN
       }
    }
}

我们甚至可以利用扩展来自动使所有的_NSObject_子类自动拥有混合器的能力。

extension NSObject: AssociatedObjects {}

现在我们有了一个坚实的基础,是时候展示一些具体的用例了,在Jobandtalent,我们已经从混合器的力量中获益。

jobandtalent.engineering/ios-archite…

长按手势

想象一下,我们有几个视图代表Jobandtalent领域中的 Job 。现在想象一下,我们想让强大的用户通过一个长按的手势来申请其中的任何职位。一种方法是让这些视图创建并保存处理长按功能所需的手势识别器。另一种......你猜对了......混杂物!

protocol LongPressApply: AssociatedObjects {
    func addLongPressApplyCapabilities(on job: Job.Id)
}

extension LongPressApply where Self: UIView {
    func addLongPressApplyCapabilities(on job: Job.Id) {
        if let longPress: UILongPressGestureRecognizer = associatedObject(for: &longPressGestureKey) {
            removeGestureRecognizer(longPress)
        }
        let handler = Handler(job: job)
        let longPress = UILongPressGestureRecognizer()
        longPress.addTarget(
            handler, 
            action: #selector(Handler.handleLongPress(g:))
        )
        addGestureRecognizer(longPress)
        
        setAssociatedObject(handler, for: &longPressHandlerKey)
        setAssociatedObject(longPress, for: &longPressGestureKey)
    }
}

private var longPressHandlerKey: UInt8 = 0
private var longPressGestureKey: UInt8 = 0

private class Handler {
    private let job: Job.Id
    init(job: Job.Id) { self.job = job }

    @objc func handleLongPress(g: UILongPressGestureRecognizer) {
        guard g.state == .began else { return }
        let alertController = UIAlertController()
        let applyAction = UIAlertAction(
            title: “Apply to job”,
            style: .default
        ) { /* Apply to job… somehow */ }
        alertController.title = name
        alertController.addAction(applyAction)
        // Show alert controller
    }
}

现在,我们可以添加长按行为来应用,如下。

class SomeJobCell: UITableViewCell, LongPressApply {
    override func awakeFromNib() {
        addLongPressApplyCapabilities(on: state.job)              
    }
}

Store订阅

如果你读过我们以前写的关于iOS架构的任何一篇文章,你就知道我们使用了一个反应式架构,其中视图订阅了一个叫做 Store 的状态持有者对象,等待状态快照被发送到它们那里。在这种情况下,我们使用一个RAII模式,其中视图被赋予一个令牌,只要他们想继续接收状态更新,他们就必须保持活力。它看起来像这样。

class SomeView {
    private let store: Store<SomeState>
    private var token: Subscription<SomeState>!

    override func viewDidLoad() {
        token = store.subscribe { state in
            render(with: state)
        }
    }
}

强制所有的视图都持有该令牌是我们想要避免的。如果有一个简单的mixin,将我们可能订阅的不同商店的不同token保存在一块状态中,怎么样?这正是下面的代码所做的。

protocol StoreSubscribing: AssociatedObjects {
    func subscribe<S>(
        _ store: Store<S>, // S is the generic state
        block: @escaping (S) -> Void
    )
}

extension StoreSubscribing {
    func subscribe<S>(
        _ store: Store<S>, 
        block: @escaping (S) -> Void
    ) {
        subscriptions[store.identifier] = store.subscribe(block)
    }
    private var subscriptions: [String: AnyObject] {
        get {
            if let subscriptions: [String: AnyObject] = associatedObject(for: &subscriptionsKey) {
                return subscriptions
            }
        
            let subscriptions = [String: AnyObject]()
            self.subscriptions = subscriptions
            return subscriptions
        }
        set {
            setAssociatedObject(newValue, for: &subscriptionsKey)
        }
    }
}

private var subscriptionsKey: UInt8 = 0

最后,使我们的视图符合mixin,我们就可以订阅商店,知道只要我们还活着,就会收到商店的状态变化通知,而不必保存任何额外的状态。

class SomeView: StoreSubscribing {
    override func viewDidLoad() {
        store.subscribe(render)
    }
}

加载和错误视图

我们的大多数视图都显示来自我们的API的异步数据。这意味着。

  • 我们应该在信息被检索时显示反馈。
  • 我们应该显示任何可能出现的问题的反馈。

代数数据类型(ADT)被用来为大多数这类视图提供信息,它是_LoadingState_的总和类型。

enum LoadingState<D, E: Error> {
    case idle
    case loading(D?)
    case loaded(D)
    case error(E, D?)
}

显示或隐藏加载和错误视图的逻辑如下。

  • loading(data == nil): 显示加载视图,隐藏错误视图。
  • loading(data != nil): 显示当前数据和加载中的指示器。
  • loaded: 隐藏加载和错误视图。
  • error(_, data == nil): 显示错误视图并隐藏加载视图。
  • error(_, data != nil): 隐藏加载和错误视图。如果需要的话,显示错误提示。

这是一个相当重要的逻辑,我们把它分散在不同的视图中,即使从视图的角度来看,通过对_LoadingState_的一些扩展,这个逻辑是相当微不足道的。

extension LoadingState {
    var shouldHideLoadingView: Bool {
        switch self {
        case .idle: fatalError(“Doesn’t apply”)
        case .loading(nil): return false
        case .loading(.some), .loaded, .error: return true
        }
    }

    var shouldHideErrorView: Bool {
        switch self {
        case .idle: fatalError(“Doesn’t apply”)
        case .loading, .loaded: return true
        case .error(_, nil): return false
        case .error(_, .some): return true // Handled via alert
        }
    }
}

这种对总和类型的方法扩展是相当方便的,它封装了我们数据上的解构逻辑,将未来重构可能对我们代码的影响降到最低。

使用方法很简单,但在每个视图中处理起来却很麻烦。

class SomeView {
    private let loadingView = LoadingView()
    private let errorView = ErrorView()

    func render(state: LoadingState<SomeData, SomeError>) {
        loadingView.isHidden = state.shouldHideLoadingView
        errorView.isHidden = state.shouldHideErrorView
    }
}

再一次,我们可以利用mixins来为我们自动处理逻辑以及加载和错误子控制器的层次结构。

让我们首先定义我们的_reactive_视图是什么样子的。我们称它们为_Stateful Views_。

protocol StatefulView: StoreSubscribing {
    associatedtype S: Equatable
    
    var state: S { get }
    func render(state: S)
}

正如你所看到的,它们基本上是可以用一块 Equatable 的状态进行渲染的视图。

有了这个,我们就可以定义 LoadingStatefulView ,一个知道如何渲染 LoadingState ADT的_StatefulView_ 。

protocol LoadingStatefulView: StatefulView 
where S == LoadingState<D, E> {
    associatedtype D
    associatedtype E
    
    func render(state: LoadingState<D, E>, content: (D) -> Void)
}

它将代表我们负责显示和隐藏错误和加载视图,让我们知道什么时候要渲染实际数据。因此,棘手的部分,正如你现在已经知道的,是在协议扩展中处理的。

extension LoadingStatefulView where Self: UIViewController {
    func render(state: S, content: (D) -> Void) {
        switch state {
        case .idle, .loading(.none):
            add(child: childLoadingViewController)
            remove(child: childErrorViewController)
        case .loading(.some(let data)), 
             .loaded(let data), 
             .error(_, .some(let data)):
            remove(child: childErrorViewController)
            remove(child: childLoadingViewController)
            content(data)
        case .error(_, .none):
            add(child: childErrorViewController)
            remove(child: childLoadingViewController)
        }
    }

    private var childLoadingViewController: UIViewController {
        return viewController(
            for: LoadingViewController(), 
            key: &loadingViewControllerKey
        )
    }

    private var childErrorViewController: UIViewController {
        return viewController(
            for: ErrorViewController(),
            key: &errorViewControllerKey
        )
    }

    private func viewController(
        for viewController: UIViewController,
        key: UnsafeRawPointer
        ) -> UIViewController {
        if let viewController: UIViewController = associatedObject(for: key) {
            return viewController
        }
    
        setAssociatedObject(viewController, for: key)
        return viewController
    }
}

private var loadingViewControllerKey: UInt8 = 0
private var errorViewControllerKey: UInt8 = 0

正如你所看到的,我们通过关联对象将加载和错误的子视图控制器附加到我们的有状态视图控制器。然后,我们通过对状态的模式匹配,简单地显示和隐藏它们。简单而强大。

最后一步是使我们的视图控制器符合 LoadingStatefulView 的要求,让混合器为我们施展魔法。

class SomeViewController: LoadingStatefulView {
    func render(state: LoadingState<SomeState, SomeError>) {
        render(state: state, content: renderContent)
    }

    private func renderContent(with state: SomeState) { ... }
}

结论

非常感谢您的阅读。我们已经看到了三个例子,在这些例子中,我们成功地利用了混合器的力量,大大简化了我们的代码。

混合器是我们所掌握的另一种强大的技术。当我们学习任何新的工具或设计模式时,总是会发生这种情况,我们往往会过度使用它。我们首先需要学习什么时候它是我们问题的正确解决方案,因为......当你是一把锤子的时候,一切看起来都像钉子

就像所有处理状态的东西一样,mixins是相当有风险的,如果用错了方式,会使我们的设计变得脆弱。要明智地使用它们。

它们像魔法一样工作,但魔法并不是免费的。因为,正如我之前所说...

大权在握,责任重大。

我们正在招聘!

如果你想了解更多关于在Jobandtalent工作的情况,你可以在这个博客文章中阅读我们一些队友的第一印象,或者访问我们的twitter


www.deepl.com 翻译