跳到内容

为什么选择 Workflow?

所以你是想让我把我必须开发的应用程序功能分解成独立的组件?然后列出每个组件的所有可能状态?还要编写类或结构体来表示这些状态,以及每个组件可能传递给其他组件的对象集合?这听起来只是为了帮助卖家订购一套礼品卡就要做很多工作!为什么要让简单的事情变得如此复杂?我为什么要使用 Workflow?

我认为即使是我们这些一直使用 Workflow 的人,最终也会问这个问题。这是一个非常合理的问题,我们在这里尝试回答。问题的核心在于 Workflow 的两个互补的理由,我们将在下面详细阐述:

  1. 软件的清晰度、正确性和可测试性(尤其是在规模化时)。
  2. 鼓励移动领域内的最佳编程范式。

软件的清晰度、正确性和可测试性(尤其是在规模化时)

我想我们大多数人都经历过:连续第二天盯着来自 200 多个客户的日志。我们知道问题是什么:用户到达屏幕 Y 时,对象 foo 的状态是 bar,但在屏幕 Y 中,foo 不应该是 bar。

为什么 foo 是 bar?

不幸的是,我们在启动屏幕 Y 时没有任何关于 foo 状态的调试日志。只有当用户尝试点击按钮 Z 时,我们才有一个日志,而此时状态已经是 bar 了,尽管它应该永远只可能是 baz 或 buz。

发生了什么?foo 是如何在屏幕 Y 上变成 bar 状态的?查看代码,foo 是与另外 15 个屏幕共享的状态,并且在所有这些屏幕中都是可变的。屏幕 Y 中更新 foo 状态的逻辑发生在与按钮 Z 的交互耦合的代码中,因此我们无法简单地为此添加单元测试,我们需要一个复杂的 UI 测试来重现屏幕 Y。我们不知道问题是如何发生的,而且似乎我们几乎无法得知其发生方式,除非付出巨大的努力!

上面的故事有点夸张,但我希望它唤起的感觉是熟悉的。要理清应用程序代码并在任何一个功能区域中建立一个足够全面的关于所有可能副作用的思维模型,是一项艰巨的任务。

现在将数字稍微扩大一下——foo 被另外 150 个屏幕共享——这项曾经艰巨的任务似乎几乎不可能完成。

所有移动开发者都面临着上述问题的某种形式,而在 Square 的 POS 应用程序中,我们每天都面临着规模化版本的问题。

我们想要什么?

  • 每个功能软件组件之间的明确边界,这些边界可以被日志记录并具有可测试的契约。
  • 特定功能软件组件之内对结果的明确预期,这些预期可以通过测试来验证其正确性。
  • 任何特定范围(例如,上述上下文中的屏幕 Y)内的不可变状态 (Immutable State),以便处理由某些事件导致的新状态 (State) 的变更的代码位于一个可以被记录和测试的“保护区域”。
  • 状态 (State) 更新与 UI 呈现 (presentation of the UI) 的明确分离。

我们之所以需要上述条件,是因为我们想要:

  1. 避免我们开始讨论的那种 Bug。换句话说,我们希望我们的测试能够给我们对应用程序逻辑的信心。
  2. 在不可避免地出现 Bug 的情况下,我们希望能够隔离场景,重现准确的条件,修复 Bug 并编写测试,以便它不再发生。

Workflow 通过提供一种模式(以及配套的应用程序运行时 Application Runtime),类似于 React、Elm 或许多其他 Web 应用程序 JavaScript 框架(更不用说即将推出的原生移动框架,如 Jetpack Compose 和 SwiftUI),来促进原生移动应用程序实现这些目标。

每个逻辑组件区域被分离成一个 Workflow,具有有限的状态集合以及在它们之间转换的逻辑。Workflow 可以组合起来形成一个完整的功能,每个 Workflow 的签名指定了明确的契约。Workflow 运行时 (Workflow Runtime) 的事件循环 (event loop) 负责为每个 Workflow 生成新的不可变状态 (immutable states),从而在 Workflow 渲染逻辑 (render logic) 中保持其不可变性。Workflow 可以以可测试的方式执行和记录,并提供了额外的钩子以便在单元测试中简单验证结果。

更简单地说,Workflow 通过为大型开发团队提供一套共享的软件组件惯用法 (idiom),以便在各个功能区域和移动平台(Android、iOS)上讨论业务逻辑,从而提高了清晰度。此外,由于应用程序由多个 Workflow 组合而成,该框架使得功能之间实现松耦合 (loose coupling),从而专注于代码更改的影响。

鼓励移动领域内的最佳编程范式

移动应用程序接收和显示大量数据!我们在 Square 的应用程序确实如此。因此,移动应用程序中出现了一种日益增长的趋势,即转向响应式编程 (reactive programming)。在这种范式中,应用程序逻辑订阅一个数据流 (stream of data),然后数据被推送到逻辑,而不是必须周期性地拉取和操作。这带来了显著的效果,确保向应用程序用户显示的数据永不过时。这种编程风格也清楚地表明,大多数移动应用程序是对数据流进行的一系列映射操作,最终映射到某个 UI 中。

另一种移动编程的最佳实践(源自悠久的传统)是偏爱声明式编程 (declarative programming),而不是命令式编程 (imperative programming)。通过这种风格选择,某个功能的代码声明了特定状态下应该发生什么,而不是由一系列本质上是如何让它发生的语句组成。这是一种最佳实践,因为当程序的逻辑以这种方式定义时,测试非常简单(因此更有可能被测试!):"对于状态 Y,我们期望渲染 Z;" "从状态 Y 开始,给定输入 A,我们期望渲染 Z+"。也许更重要的是,与一系列复杂的计算机指令相比,它更容易阅读、快速理解和推理。

Workflow 鼓励声明式风格,因为必须列举特定组件的每个状态 (State),然后为该特定状态声明渲染 (Rendering)(传递给 UI 框架的表示),同时声明在该状态下应该运行哪些子 Workflow (children) 和副作用 (side effects)。经过良好测试且可靠的 Workflow 运行时循环 (runtime loop) 本身负责处理如何启动和停止子 Workflow 和副作用,从而减少资源泄露 (resource leaks)。通过要求对每个状态 (State)、渲染 (Rendering) 和将改变当前状态的动作 (Actions) 进行这些正式定义,Workflow 自然而然地鼓励了声明式编程。

虽然响应式编程 (reactive programming) 和声明式编程 (declarative programming) 可能是当前的最佳实践,但有一项软件工程原则已被一次又一次地证明是规模化系统中最普遍、最重要的一项:关注点分离 (Separation of Concerns)。任何规模化系统都需要多个独立的组件,这些组件可以由多支团队独立地进行开发、测试、改进和重构。多组件系统需要通信,而任何良好的通信都需要明确的结构和契约。


在 Square 的移动应用程序中,我们已确定 Model-View-ViewModel (MVVM) 架构作为应用程序各层按主题分离关注点 (topical separation of concerns) 的结构。MVVM 的单向分层通信 (unidirectional layered communication) 与 Model-View-Presenter (MVP) 相同,这与 Model-View-Controller (MVC) 的“循环”通信相反。MVVM 在 ViewModel 和 View 之间使用严格绑定 (strict binding),这与 MVC 相同,而不是 MVP 中对 Model 的命令式解释。MVVM 提供了单向数据流 (unidirectional data flow) 的推理和理解优势,同时尽可能地从视图层消除了业务逻辑,并鼓励声明式 ViewModel (declarative ViewModels)。在 Square,这种方式效果很好,因为我们的 UI 设计框架变化不频繁(因此保持绑定最新不是很大的开销),但业务逻辑不断更新(因此强调低耦合 (low coupling) 很重要)。

Workflow 采纳 MVVM,因为 Workflow 树生成的渲染 (Rendering) 就是 ViewModel,它可以绑定到任何原生移动 UI 框架。

对于基于功能点分离关注点 (feature based separation of concerns),我们依靠 Workflow 通过强的父子约定 (strong parent-child contracts) 和分层树状组织 (hierarchical tree organization) 实现规模化组合 (composition at scale) 的能力。


虽然构建一个 Hello World Workflow 可能看起来像是大炮打蚊子(尽管实际上也没那么糟!),但 Workflow 对开发者要求的明确性和契约为良好的沟通奠定了结构。Workflow 的可组合性 (composability) 鼓励复用 (reuse),并鼓励将关注点分离到最合适的、可复用的组件中。

还有更多特定于平台的最佳实践,Workflow 与之很好地契合,例如使用 Kotlin 协程 (Kotlin coroutines) 实现的结构化并发 (structured concurrency),因为每个 Worker 或副作用 (side effect) 都可以为操作定义特定的协程作用域 (coroutine scope)。

那么未来十年呢?Jetpack Compose UI 和 SwiftUI 正在成为未来原生移动 UI 工具包 (UI toolkits) 的主流。它们都采用了与 Workflow 相同的 MVVM 方法 (MVVM approach),并鼓励思考应用程序独立组件的“可组合性 (composability)”。有了这种共鸣 (resonance),Workflow 可以帮助你准备好思维模型 (mental model),以适应这些新的 UI 工具包,并以一种方式塑造我们的代码库,从而简化我们的采纳过程。要了解更多关于 Compose 和 Workflow 的信息,请参阅这篇文章