Agera官方Wiki翻译(五)——已编译源

复杂源(complex repository)可以使用一个表达式编译(compiled)表示。表达式包含如下部分:

  1. Repositories.repositoryWithInitialValue(...);
  2. 事件源 .observe(...)
  3. 响应频率 .onUpdatesPer(...) or .onUpdatesPerLoop()
  4. 数据流程 .getFrom(...), .mergeIn(...), .transform(...), etc.;
  5. 杂项设置 .notifyIf(...), .onDeactivation(...), etc.;
  6. .compile().

当已编译源被激活,它会将自己内部的 updatable 注册到给定事件源,并启动数据处理流程来计算初始值。当从事件源收到事件的时候,这个流程再次启动。表达式的第一步分指定的值,是在初始值尚未被计算出来时的取值。当数据更新完成时,源的客户会被通知。当源不活动,内部 updatable 会从事件源取消注册,数据处理流程不在执行,因此对外暴露的值可能会过时。一旦重新激活,这个值又会再次保持最新。

表达式的不同阶段通过 RepositoryCompilerStates 内嵌的编译器状态接口(compiler state interfaces)来表示,这样在每个阶段只暴露合适的方法,来引导开发者正确的完成表达式(当使用 IDE 的自动补全特性)。方法的完整文档可以在这些接口中找到;特别是每个部分:

  • 事件源和响应频率:RFrequency 和它的父接口 REventSource
  • 数据处理流程:RFlow 和它的父接口 RSyncFlow
  • 其他设置:RConfig

源编译表达式不允许从中间断开,来将中间结果存到本地变量或者 cast 到另一个接口。这些方法都是不支持的。

编译一个源会有一点麻烦,但是之后的操作就简单了。repository 最适合与高级组件的生命周期进行绑定,例如 Activity、可重用的视图或者一个为整个 APP 服务的全局单例。通过源的编译操作就能实现(编译实在运行时发生的)。

何时、何处、何物

已编译源表达式清楚地说明了源何时响应它的数据源,何处(哪个线程)进行响应,何物是指如何构造暴露数据。

已编译源以给定的频率观察给定的数据源。表达式的这两个部分构成了以编译源的“何时”这个概念。

数据处理流程制定了数据源(依赖)并计算出源的数据。这是已编译源的“何物”的概念。

由于使用了内部 updatable,它必须在 Looper 线程上被注册到事件源,已编译源与 worker Looper 相联系(在下面的异步编程一节将详细介绍)。在数据处理过程中,可以通过插入指令将处理过程移至 Java 的 Executors。这些明确的线程结构组成了已编译源的“何处”这个概念。

数据处理流程

数据处理流程由指令组成。每个指令接受一个输入值并为下一条指令产生一个输出值。第一条指令的输入值类型为源的值类型,同样的还有最后一条指令的输出值。编译状态接口(compiler state interfaces)帮助你确保范型参数的类型安全,允许输入类型协变(contravariance)(下一条指令可以接受前一条类型产生的值的父型)并且允许输出类型 covariance (最后一条指令可以产生源值类型的子型)。

当数据处理流程正在运行的时候,当前源的值通过 Repository.get() 被暴露出来,用来作为第一条指令的输入值。如果流程在之前没有更新值,那么这个值就是源的初始值,或者源被 RepositoryConfig.RESET_TO_INITIAL_VALUE 设置重置。指令顺序地执行对输入值做变换。数据处理流程通常在运行完 then 指令后结束并产生一个终值,或者运行 termination clause (通过 RTermination 状态接口表示,详见下文“尝试与结果”一节)给出一个值并终止流程,在这种情况下,源的值会被更新,已注册的 updatables 会被通知。流程还可以使用 .thenSkip() 指令终止,或者通过终止指令(termination clause)跳过剩下的流程,在这种情况下将不会更新值也不会通知更新。

操作符

为了允许数据处理流程调用客户代码逻辑,Agera 制定了下列接口,每个接口带有一个方法:

  • Supplier.get():0 输入,1 输出操作符;
  • Function.apply(TFrom):1 输入,1 输出操作符
  • Merger.merge(TFirst, TSecond):2 输入,1 输出操作符。

使用到它们的指令有:

  • .getFrom(Supplier) 及变形;
  • .transform(Function) 及变形;
  • .mergeIn(Supplier, Merger) 及变形。

可以使用下图表示:

operators

对于高级功能,数据处理流程提供了非线性操作符(数据从侧面进来,或者终止流程),有下列接口提供:

  • Receiver.accept(T): 1 输入,0 输出操作符;
  • Binder.bind(TFirst, TSecond): 2 输入,0 输出操作符;
  • Predicate.apply(T): 检查输入并给出是、非结果的操作符。

使用到它们的操作符有:

  • .sendTo(Receiver) 及变形;
  • .bindWith(Supplier, Binder) 及变形;
  • .check(Predicate).or… 及变形,

可使用下图表示:

sideoperators

为实现模块化结构,Repository 实现了 SupplierMutableRepository 同时实现了 SupplierReceiver,因此它们可以在复杂源当中直接当作操作符使用。

尝试与结果

函数式接口 SupplierFunctionMerger 都被定义为不抛出任何异常,但是在实际中,许多操作都会执行失败。要捕获这些失败,Agera 提供了一个封装类 Result,它能封装一个容易失败操作或者一个 attempt 的结果(包括成功与否)。然后,attempt 可以作为 SupplierFunctionMerger 实现,返回 Result

数据处理流程提供能够感知错误的指令,这样就允许在出错的时候终止流程:

  • .attemptGetFrom(Supplier).or…;
  • .attemptTransform(Function).or…;
  • .attemptMergeIn(Supplier, Merger).or…,

其中 .or…(跟 .check 指令的第二个部分相同)是中指语句,用 RTermination 状态接口来表示,这已在上文中说过,它允许跳过更新(.orSkip())或者在新值被计算出来之前终止数据处理流程(.orEnd(Function))。

这些 .attempt* 指令保证下一条指令只接收成功的结果,因此使用 Result<T> 操作符产生的能够感知失败的指令的输出类型,是 T 而非 Result`。

对称的,一个操作符也可以是 recovery 操作符,意味着它将 Result 作为输入,或者甚至是一个 attempt recovery 操作符,这意味着它同时接收与产生 Result。要在数据处理流程中使用这个操作符,前面的指令必须都是不感知失败的(即使操作符是一个 attempt 操作符),这样 recovery 操作符可以从前面指令中收到成功和失败的结果(类型为 Result)。

异步编程

源必须在 Looper 线程被编译(通常是主线程)。Looper 成为源的工作 Looper,所有的过程都运行在这个 Looper 线程:

  • 客户 updatable 的注册与取消注册;
  • 观察、处理和事件源更新频率限制;
  • 启动数据处理流程

数据处理流程不需要 Looper 线程的完全异步。特殊指令 .goTo(Executor).goLazy() 会启动异步编程。他们不改变输入值;他们在运行时控制流程的延续:.goTo(Executor) 将剩下的执行指令送到 Executor,而 .goLazy() 暂停执行指导在 Repository.get() 被首次调用对新值有需求的时候。

.goTo(Executor) 指令将工作 Looper 线程空出来使其能够处理其它事件,源可能会并发地被 clients 设为不活动状态,或者会并发收到来自事件源的更新通知。在后一种情况下,数据处理流程会被调度重新运行,而非在运行中的流程同时再起一个并行流程,这样为了减少竞争条件。愿可以设置为在不活动时以及并发更新时取消正在进行的流程。这有助于珍惜资源(在取消激活的情况)以及更快的重新运行(并发更新情况)。取消的行为通过 .onDeactivation(int).onConcurrentUpdate(int) 方法来配置,在 RConfig 状态接口中被定义。

对于 .goLazy() 指令,注册上来的 updatables 在更新时被通知,但是由 updatables 根据是否需要更新源值来决定是否运行后续指令。流程在 Repository.get() 被调用的线程并发继续运行,并且因为这个方法必须产生一个值,这时所有的取消信号都被忽略。另一方面,如果源在 Repository.get() 继续暂停的流程之前,从事件源收到一个 update,暂停状态和以保存的中间结果都会被丢弃,剩下的指令不会再运行,流程会立刻重启。在流程重启之后且在再次到达 .goLazy() 之前调用 Repository.get() 会返回上一次源的值。因此 .goLazy() 有助于跳过不必要的计算,因此有策略地使用可以提高程序的运行。