Reactive​Cocoa

编程语言是有生命的。语言在自由无方向迅速发展的生命周期中不断被推动、被挑战、被变得不规范化、或被蒙上了神秘面纱。科技在不停的改变中、在开发团队和开源社区不断来了又走中得以不断发展;隐晦的神秘力量凭借新兴项目的巨人肩膀被磨练得力量日益突出,很快就会在长期的蛰伏后觉醒,大力开辟出一片新天地。

Objective-C 在几十年间的非凡发展史可以分为四个阶段:

第 1 阶段,NeXT 接手了 Objective-C 用以支持NeXTSTEP世界上第一个 web server

第 2 阶段,苹果并购了 NeXT,(在与 Java 的长期拉锯战之后),Objective-C 处于苹果技术栈的核心地位。

第 3 阶段,随着 iOS 系统的发布,Objective-C 上升到了空前重要的地位,成为移动计算领域最重要的语言。

Objective-C 的第 4 阶段,也就是现如今,伴随着大批从 Ruby、Python、Javascript 社区转型的 iOS 工程师的涌入,Objective-C 开始在开源领域大放异彩。Objective-C 第一次直接被苹果以外的其他人打磨和引导。

打破了苹果 API 排他性的盾牌,本期 NSHipster 将介绍一个为 Objective-C 勇敢构建新纪元的开源项目:ReactiveCocoa


为了对 ReactiveCocoa 有全方位了解,请查看其项目的READMEFramework OverviewDesign Guidelines

ReactiveCocoa是一个将函数响应式编程范例带入 Objective-C 的开源库。由Josh AbernathyJustin Spahr-Summers在对GitHub for Mac的开发过程中建立。上周,ReactiveCocoa 发布了其1.0 release,达到了第一个重要里程碑。

函数响应式编程(Functional Reactive Programming a.k.a FRP)是思考软件将输入转化为输出在时间上的持续过程的一种方式。Josh Abernathy 这样解释它

程序接收输入产生输出。输出就是对输入做了一些事的结果。输入,转换,输出,完成。

输入是应用动作的全部来源。点击、键盘事件、定时器事件、GPS 时间、网络请求响应都算是输入。这些事件被传递到应用中,应用将他们以某种方式混合,产生了结果:就是输出。

输出通常会改变应用的 UI。开关状态变化、列表有了新的元素都是 UI 变化。也有可能让磁盘上某个文件产生变化,或者产生一个 API 请求,这都是应用的输出。

但不像传统的输入输出设计,应用的输入输出可以产生很多次。应用打开后,不只是一个简单的 输入 → 工作 → 输出 就构成了一个生命周期。应用经常有大量的输入并基于这些输入产生输出。

为了举例说明传统范式即 Objective-C 的命令响应式编程和函数响应式范式的区别,来思考一下下面这个判断注册项是否合法的常用样例:

传统范式

- (BOOL)isFormValid {
    return [self.usernameField.text length] > 0 &&
            [self.emailField.text length] > 0 &&
            [self.passwordField.text length] > 0 &&
            [self.passwordField.text isEqual:self.passwordVerificationField.text];
}

#pragma mark - UITextFieldDelegate

- (BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
{
    self.createButton.enabled = [self isFormValid];

    return YES;
}

传统范式的样例中,逻辑被放在了很多方法里,零碎地摆放在 view controller 里,通过到处散布到 delegate 里的self.createButton.enabled = [self isFormValid];方法在页面的生命周期中被调用。

比较一下用 ReactiveCocoa 写的同样功能的代码:

ReactiveCocoa

RACSignal *formValid = [RACSignal
  combineLatest:@[
    self.username.rac_textSignal,
    self.emailField.rac_textSignal,
    self.passwordField.rac_textSignal,
    self.passwordVerificationField.rac_textSignal
  ]
  reduce:^(NSString *username, NSString *email, NSString *password, NSString *passwordVerification) {
    return @([username length] > 0 && [email length] > 0 && [password length] > 8 && [password isEqual:passwordVerification]);
  }];

RAC(self.createButton.enabled) = formValid;

所有对于判断表单输入是否合法的逻辑都被整合为一串逻辑了。每次不论哪个输入框被修改了,用户的输入都会被 reduce 成一个布尔值,然后就可以自动来控制注册按钮的可用状态了。

概述

ReactiveCocoa 由两大主要部分组成:signals (RACSignal) 和 sequences (RACSequence)。

signal 和 sequence 都是streams,他们共享很多相同的方法。ReactiveCocoa 在功能上做了语义丰富、一致性强的一致性设计:signal 是push驱动的 stream,sequence 是pull驱动的 stream。

RACSignal

  • 异步控制或事件驱动的数据源:Cocoa 编程中大多数时候会关注用户事件或应用状态改变产生的响应。
  • 链式以来操作:网络请求是最常见的依赖性样例,前一个对 server 的请求完成后,下一个请求才能构建。
  • 并行独立动作:独立的数据集要并行处理,随后还要把他们合并成一个最终结果。这在 Cocoa 中很常见,特别是涉及到同步动作时。

Signal 会触发它们的 subscriber 三种不同类型的事件:

  • 下一个事件从 stream 中提供一个新值。不像 Cocoa 集合,它是完全可用的,甚至一个 signal 可以包含 nil
  • 错误事件会在一个 signal 结束之前被标示出来这里有一个错误。这种事件可能包含一个 NSError 对象来标示什么发生了错误。错误必须被特殊处理——错误不会被包含在 stream 的值里面。
  • 完成事件标示 signal 成功结束,不会再有新的值会被加入到 stream 当中。完成事件也必须被单独控制——它不会出现在 stream 的值里面。

一个 signal 的生命由很多下一个(next)事件和一个错误(error)完成(completed)事件组成(后两者不同时出现)。

RACSequence

  • 简化集合转换:你会痛苦地发现 Foundation 库中没有类似 mapfilterfold/reduce 等高级函数。

Sequence 是一种集合,很像 NSArray。但和数组不同的是,一个 sequence 里的值默认是延迟加载的(只有需要的时候才加载),这样的话如果 sequence 只有一部分被用到,那么这种机制就会提高性能。像 Cocoa 的集合类型一样,sequence 不接受 nil 值。

RACSequence 允许任意 Cocoa 集合在统一且显式地进行操作。

RACSequence *normalizedLongWords = [[words.rac_sequence
    filter:^ BOOL (NSString *word) {
        return [word length] >= 10;
    }]
    map:^(NSString *word) {
        return [word lowercaseString];
    }];

Cocoa 中的先例

Capturing and responding to changes has a long tradition in Cocoa, and ReactiveCocoa is a conceptual and functional extension of that. It is instructive to contrast RAC with those Cocoa technologies:

RAC 与 KVO

Key-Value Observing是 Cocoa 所有魔法的核心,它被广泛应用在 ReactiveCocoa 对于属性变化的影响动作中。然而 KVO 用起来即不简单也不开心:它的 API 有很多过度设计的参数,以及缺乏方便的 block 方式调用。

RAC 与 Bindings

Bindings也是黑魔法。

虽然对 OS X 控制的要点就是 Bindings,但是它的意义在近年来越来越没那么重要了,因为焦点已经移动到了 iOS 和 UIKit 这些 Bindings 不支持的东西身上。Bindings 替代了大量的模版胶水代码,允许在 Interface Builder 中完成编码,但严格上说还是比较有局限性的,并且_无法_debug。RAC 提供了一种简洁易懂、扩展性强的以代码为基础的 API 来运行在 iOS 上,目标就是取代所有在 OS X 能用 Bindings 实现的神奇功能。


Objective-C 在 C 的核心上吸收了 Smalltalk 的思想建立而成,但哲学理念上已经超越了它原本来源的血统。

@protocol 是对 C++多重继承的拒绝,顺应抽象数据的类型范式是对 Java Interface的吸收。Objective-C 2.0 引入了@property / @synthesize则灵感来自 C#的 get; set; 方法对 getter 和 setter 的速记(就语法上来说,这也是 NeXTSETP 强硬路线坚持者经常辩论的一点)。Block 给这门语言带来了函数式编程的好处,可以使用 Grand Central Dispatch——来自 Fortran / C / C++ standard OpenMP思想而成的基于队列的并发 API。下标和对象字面量都是像 Ruby、Javascript 这样的脚本语言的标准特性,如今也由一个 Clang 插件被带入了 Objective-C 的世界里。

ReactiveCocoa 则给 Objective-C 带来了函数响应式编程的健康药剂。它本身也是受 C#的Rx libraryClojureElm的影响发展而成。

好的点子会传染。ReactiveCocoa 就是一种警示,提醒人们好的点子也可以从看似不太可能的地方传播过来,这样的新鲜思想对解决类似的问题也会有完全不同的方法呢。

作者 Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon. He is the founder of NSHipster and Flight School, and the creator of several open source libraries, including AFNetworking and Alamofire.

翻译者
Croath Liu

云游四海的工程师

下一篇文章

编程结合了人类思考过程中众多学科,从高层次的辩论和语义学——我们用来解释一个系统如何工作的故事——到支撑所有一切的数学和哲学机器。