ReactiveCocoaMattt Croath Liu 🚩🌱

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

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


除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。