下载本文的PDF版本 PDF

变化的动力:为什么响应式至关重要

通过将每个关注点集中在其自身的模块中来驯服变化的动力。


Andre Medeiros

专业编程是关于大规模处理软件。当问题规模小且受限时,一切都很简单:可以使用命令式编程或函数式编程或任何其他范例优雅地解决。当程序员必须处理大量数据、网络请求或相互交织的实体(如 UI(用户界面)编程中)时,真正的世界性挑战就出现了。

在这些不同类型的挑战中,管理代码库中变化的动力是一个常见的挑战,可能在 UI 编程或后端中遇到。如何在需要用新信息相互更新的多个参与方之间构建控制流和并发,这被称为管理变化。在 UI 程序和服务器中,并发通常是存在的,并且是大多数挑战和复杂性的根源。

有些复杂性是偶然的,可以消除。当本质复杂性的量很大时,管理并发复杂性变得困难。在这些情况下,实体之间的相互关系是复杂的——并且无法简化。例如,需求本身可能已经代表了本质复杂性。在一个在线文本编辑器中,仅需求就可能决定键盘输入需要更改视图、更新文本格式、或许还需要更改目录、字数统计、段落计数、请求保存文档以及采取其他操作。

由于本质复杂性无法消除,因此替代方案是使其尽可能易于理解,从而使其可维护。当涉及到围绕某个实体 Foo 的变化复杂性时,您想了解 Foo 更改了什么,什么可以更改 Foo,以及哪个部分负责更改。

变化如何从一个模块传播到另一个模块

图 1 是电子商务软件代码库的数据流图,其中矩形代表模块,箭头代表通信。这些模块是按照需求互连的,而不是按照架构决策互连的。每个模块可能是一个对象、一个面向对象的类、一个参与者或一个线程,具体取决于所使用的编程语言和框架。

Dynamics of Change: Why Reactivity Matters

Cart 模块到 Invoice 模块的箭头(图 2a)意味着购物车以有意义的方式更改或影响发票中的状态。这种情况的一个实际例子是,每当向购物车添加新产品时,重新计算总发票金额的功能(图 2b)。

Dynamics of Change: Why Reactivity Matters

箭头从 Cart 开始,到 Invoice 结束,因为 Cart 内部的操作可能会导致 Invoice 的状态发生变化。箭头表示 CartInvoice 之间变化的动力。

假设所有代码都存在于某个模块中,则箭头不能存在于两者之间的空间中;它也必须存在于模块中。箭头是在 Cart 中还是在 Invoice 中定义的?这取决于程序员来决定。

被动式编程

通常将箭头定义放在箭头尾部:购物车。Cart 中处理添加新产品的代码通常负责触发 Invoice 更新其发票数据,如图表和图 3 中的 Kotlin (https://kotlinlang.org/) 代码片段所示。

Dynamics of Change: Why Reactivity Matters

Cart 承担了主动角色,而 Invoice 则扮演了被动角色。虽然 Cart 负责更改和保持 Invoice 状态的最新,但 Invoice 没有代码表明更新来自 Cart。相反,它必须公开 updateInvoicing 作为公共方法。另一方面,购物车没有访问限制;它可以自由选择 ProductAdded 事件应该是私有的还是公共的。

让我们将这种编程风格称为被动式编程,其特点是远程命令式更改和对状态管理的委托责任。

响应式编程

定义箭头所有权的另一种方式是响应式编程,其中箭头在箭头头部定义:Invoice,如图 4 所示。在这种设置中,Invoice 监听购物车中发生的 ProductAdded 事件,并确定它应该更改其自身的内部发票状态。

Dynamics of Change: Why Reactivity Matters

Cart 现在承担了广播角色,而 Invoice 则扮演了响应式角色。Cart 的职责是执行其对购买产品的管理,同时提供产品已添加或删除的通知。

因此,Cart 没有代码明确表明其事件可能会影响 Invoice 中的状态。另一方面,Invoice 负责保持其自身的发票状态最新,并将 Cart 作为依赖项。

责任现在颠倒了,Invoice 可以选择使其 updateInvoicing 方法是私有的还是公共的,但 Cart 必须使 ProductAdded 事件公开。图 5 说明了这种二元性。

Dynamics of Change: Why Reactivity Matters

术语响应式在 1989 年由 Gérard Berry1 模糊地定义。这里给出的定义足够广泛,可以涵盖现有的响应式系统概念,例如电子表格、Actor 模型、响应式扩展 (Rx)、事件流等。

被动式与响应式:管理本质复杂性

在模块和变化通信箭头的网络中,箭头应该在哪里定义?何时应该使用响应式编程,何时被动模式更合适?

在尝试理解复杂的模块网络时,通常需要问两个问题

• 模块 X 更改了哪些模块?

• 哪些模块可以更改模块 X?

答案取决于使用了哪种方法:响应式或被动式,或两者兼而有之。为了简单起见,假设无论选择哪种方法,它都会在整个架构中统一应用。

例如,考虑图 6 所示的电子商务模块网络,其中被动模式无处不在。

Dynamics of Change: Why Reactivity Matters

要回答关于 Invoice 模块的第一个问题(发票更改了哪些模块?),您只需要查看 Invoice 模块中的代码,因为它拥有箭头并定义了如何从 Invoice 内部作为主动组件远程更改其他模块。

但是,要发现哪些模块可以更改 Invoice 的状态,您需要查找整个代码库中 Invoice 的公共方法的所有用法。

实际上,当多个其他模块可能更改 Invoice 时,这变得难以维护,这在本质上复杂的软件中是常见的。这可能导致程序员必须构建一个心理模型,了解多个模块如何并发地修改所讨论模块中的一段状态。另一种替代方案是在任何地方应用响应式模式,如图 7 所示。

Dynamics of Change: Why Reactivity Matters

要发现哪些模块可以更改 Invoice 的状态,您只需查看 Invoice 模块中的代码,因为它包含定义依赖关系和变化动力的所有“箭头”。当所有相关实体都位于同一位置时,构建并发变化的心理模型会更容易。

另一方面,关于发现 Invoice 影响了哪些其他模块的双重问题,只能通过搜索 Invoice 模块的公共广播事件的所有用法来回答。

当排列在表格中时,如图 8 所示,这些描述的被动式和响应式属性是彼此对偶的。

Dynamics of Change: Why Reactivity Matters

您选择的模式取决于在处理特定代码库时,程序员更常考虑这两个问题中的哪一个。然后,您可以选择对最常见问题的答案是“向内看”的模式,因为您希望能够快速找到答案。集中的答案比分散的答案更好。

虽然这两个问题在平均代码库中都很重要,但更常见的需求可能是了解特定模块的工作方式。这就是响应式重要性的原因:在查看模块影响什么之前,您通常需要了解模块的工作方式。

因为仅被动式方法会生成不负责任的模块(它们将其状态管理委托给其他模块),所以仅响应式方法是更明智的默认选择。也就是说,被动模式适用于数据结构和创建所有权层次结构。面向对象编程中的任何常见数据结构(例如哈希映射)都是被动模块,因为它公开了允许更改其内部状态的方法。因为它将回答问题“它何时更改?”的责任委托给包含数据结构对象的任何模块,所以它创建了一个层次结构:包含模块作为父模块,数据结构作为子模块。

管理依赖关系和所有权

使用仅响应式方法,每个模块都必须静态地定义其对其他模块的依赖关系。在 CartInvoice 示例中,Invoice 将需要静态导入 Cart。因为这适用于所有地方,所以所有模块都必须是单例。实际上,Kotlin 的 object 关键字(在 Scala 中也是如此)用于创建单例。

在图 9 的响应式示例中,关于依赖关系有两个关注点

• 依赖关系是什么:由 import 语句定义。

• 如何依赖:由事件监听器定义。

Dynamics of Change: Why Reactivity Matters

单例作为依赖关系的问题仅与响应式模式中的什么关注点有关。您仍然希望保留响应式风格的如何将依赖关系组合在一起,因为它适当地回答了问题“模块如何工作?”

虽然是响应式的,但被更改的模块通过导入静态地意识到其依赖关系;虽然是被动式的,但被更改的模块不知道其依赖关系。

到目前为止,本文分析了仅被动式和仅响应式方法,但在两者之间存在混合两种范例的机会:仅保留响应式的如何优势,同时使用被动式编程来实现什么关注点。

Invoice 模块可以被动地处理其依赖关系:它公开一个公共方法以允许另一个模块设置或注入依赖关系。同时,Invoice 可以响应式地处理其工作方式。如图 10 中的示例代码所示,这产生了一个混合的被动响应式解决方案

• 它是如何工作的?向内看(响应式)。

• 它依赖于什么?通过公共方法注入(被动式)。

Dynamics of Change: Why Reactivity Matters

这将有助于使模块更具可重用性,因为它们不再是单例。让我们看另一个示例,其中典型的被动式设置转换为被动响应式设置。

示例:分析事件

以仅被动式风格编写 UI 程序的代码是很常见的,其中程序的每个不同屏幕或页面都使用 Analytics 模块的公共方法向 Analytics 后端发送事件。图 11 中的示例代码说明了这一点。

Dynamics of Change: Why Reactivity Matters

为分析事件构建仅被动式解决方案的问题在于,每个页面都需要具有与分析相关的代码。此外,要理解分析的行为,您必须研究分散在整个代码中的分析。希望将分析方面与核心功能和关于页面(如 LoginPage)的业务逻辑分开。面向切面编程2 是解决此问题的一种尝试,但也可以通过带有事件的响应式编程来分离方面。

为了使代码库仅是响应式的,Analytics 模块将需要静态地依赖于程序中的所有页面。相反,您可以使用被动响应式解决方案来使 Analytics 模块通过公共注入方法接收其依赖关系。这样,控制页面路由的父模块也可以使用有关这些页面的信息来引导分析。请参见图 12 中的示例。

Dynamics of Change: Why Reactivity Matters

注意箭头

在架构中引入响应式模式可以帮助更好地定义哪个模块拥有两个模块之间变化的关系。本质上复杂的需求的软件架构通常是关于在模块中构建代码,但不要忘记模块之间的箭头也存在于模块中。一定程度的响应式很重要,因为它创建了关注点分离。特定模块应负责其自身的状态。这在事件驱动的架构中很容易实现,在事件驱动的架构中,模块不会侵入性地更改彼此。通过将每个关注点集中在其自身的模块中来驯服变化的动力。

参考文献

1. Berry, G. 1989. 实时编程:专用或通用语言。[研究报告] RR-1065。INRIA(法国国家信息与自动化研究所);https://hal.inria.fr/inria-00075494/document。

2. Kiczales, G., Lamping, J., Mendhekar, A., Maeda, C., Lopes, C., Loingtier, J. M., Irwin, J. 1997. 面向切面编程。面向对象编程第 11 届欧洲会议论文集:220-242。

Andre Medeiros 是 Futurice 的 Web 和移动开发人员。他以参与用户界面的响应式编程而闻名,特别是使用 ReactiveX 库。Medeiros 构建了 JavaScript 库和工具,例如 Cycle.js 和 RxMarbles。他拥有理论计算机科学硕士学位。

版权所有 © 2016 归所有者/作者所有。出版权已许可给 。

acmqueue

最初发表于 Queue 第 14 卷,第 3 期
数字图书馆 中评论本文





更多相关文章

Catherine Hayes, David Malone - 质疑非密码哈希函数的评估标准
虽然密码哈希函数和非密码哈希函数无处不在,但在它们的设计方式上似乎存在差距。密码哈希存在许多由各种安全需求驱动的标准,但在非密码方面,存在一定程度的民间传说,尽管哈希函数历史悠久,但尚未得到充分探索。虽然针对真实世界数据集的均匀分布很有意义,但当面对具有特定模式的数据集时,这可能是一个挑战。


Nicole Forsgren, Eirini Kalliamvakou, Abi Noda, Michaela Greiler, Brian Houck, Margaret-Anne Storey - DevEx 行动
随着领导者寻求在财政紧缩和人工智能等变革性技术的背景下优化软件交付,DevEx(开发者体验)在许多软件组织中越来越受到关注。技术领导者凭直觉接受,良好的开发者体验能够实现更有效的软件交付和开发者幸福感。然而,在许多组织中,改进 DevEx 的拟议举措和投资难以获得支持,因为业务利益相关者质疑改进的价值主张。


João Varajão, António Trigo, Miguel Almeida - 低代码开发效率
本文旨在通过展示使用基于代码、低代码和极限低代码技术进行的实验室实验结果来研究生产力差异,从而为该主题提供新的见解。低代码技术已清楚地显示出更高的生产力水平,为低代码在短期/中期内主导软件开发主流提供了强有力的论据。本文报告了程序和协议、结果、局限性和未来研究的机会。


Ivar Jacobson, Alistair Cockburn - 用例至关重要
虽然软件行业是一个快节奏且令人兴奋的世界,其中不断开发新的工具、技术和方法来服务于商业和社会,但它也很健忘。在其快速前进的匆忙中,它容易受到时尚的支配,并且可能会忘记或忽略针对其面临的一些永恒问题的成熟解决方案。用例,于 1986 年首次引入并在之后普及,就是这些成熟的解决方案之一。





© 保留所有权利。

© . All rights reserved.