反应式编程的界限在哪里
Where to draw the line with reactive programming
我已经在我的项目中使用 RxJava 大约一年了。
随着时间的推移,我变得非常喜欢它 - 现在我想也许太多了......
我现在写的大多数方法里面都有某种形式的 Rx,这太棒了! (直到不是)。
我现在注意到有些方法需要大量工作来组合不同的可观察生成方法。
我有一种感觉,虽然我现在理解我写的东西,但下一个程序员将很难理解我的代码。
在进入底线之前,让我直接从我的 Kotlin 代码中举一个例子(不要深入研究它):
private fun <T : Entity> getCachedEntities(
getManyFunc: () -> Observable<Timestamped<List<T>>>,
getFromNetwork: () -> Observable<ListResult<T>>,
getFunc: (String) -> Observable<Timestamped<T>>,
insertFunc: (T) -> Unit,
updateFunc: (T) -> Unit,
deleteFunc: (String) -> Unit)
= concat(
getManyFunc().filter { isNew(it.timestampMillis) }
.map { ListResult(it.value, "") },
getFromNetwork().doOnNext {
syncWithStorage(it.entities, getFunc, insertFunc, updateFunc, deleteFunc)
}).first()
.onErrorResumeNext { e -> // If a network error occurred, return the cached data and the error
concat(getManyFunc().map { ListResult(it.value, "") }, error(e))
}
简而言之,它的作用是:
- 从存储中检索一些带时间戳的数据
- 如果数据不是新的,从网络获取数据
- 再次与存储同步网络数据(以更新它)
- 如果发生网络错误,再次检索旧数据和错误
我的实际问题来了:
响应式编程提供了一些非常强大的概念。但据我们所知 with great power comes great responsibility
.
我们的底线在哪里?用很棒的反应性单行代码填充我们的整个程序是否可以,还是我们应该只为真正平凡的操作保存它?
显然这是非常主观的,但我希望有更多经验的人可以分享他的知识和陷阱。
让我措辞更好
How do I design my code to be reactive yet easy to read?
我发现在编写 Rx(或任何温和的 sophisticated/new 技术)时我会记住两件事
- 我可以测试吗?
- 我可以轻松雇用可以维护它的人吗?维护不费力,一个人维护就好了?
为此,我还发现仅仅因为你可以,并不总是意味着你应该。作为指南,我尽量避免创建超过 7 行代码的查询。比这更大的查询,我尝试分成我编写的子查询。
如果您提供的代码处于代码库的核心,并且处于复杂性的极端,那么它可能没问题。但是,如果您发现所有 Rx 代码都如此复杂,您可能会创建一个难以使用的代码库。
当你拿起 Rx 时,它会变得闪闪发亮hammer,一切都开始看起来像生锈的钉子,等待你敲打。
就我个人而言,我认为最大的线索在于名称,reactive 框架。鉴于需求,您需要反思反应式解决方案是否真正有意义。
在任何 Rx 命题中,您都希望引入一个或多个事件流并执行一些响应事件的操作。
我觉得有两个关键问题要问:
- 你在控制事件流吗?
- 您必须以事件流的速率完成响应到什么程度?
如果您没有控制事件流并且您必须 以事件流的速率响应,那么 Rx 是一个很好的候选者。
在任何其他情况下,这可能是一个糟糕的选择。
我见过很多这样的例子,人们为了证明 Rx 的合理性而跳过重重障碍来制造缺乏控制的错觉——这对我来说似乎很疯狂。为什么要放弃你拥有的控制权?
一些例子:
您必须从固定的文件列表中提取数据并将其存储在数据库中。您决定将每个文件名推送到一个主题中,并创建一个反应式管道来打开每个文件并投影数据,然后以某种方式处理数据并最终将其写入数据库。
这未通过控制测试和速率测试。遍历文件并拉它们并尽可能快地处理它们会容易得多。 “决定推动”这句话是这里的赠品。
您需要显示证券交易所的股票价格。
显然这对 Rx 来说是个不错的选择。如果你跟不上总体价格的变化,那你就完蛋了。它 可能 是您合并价格的情况(也许每秒只提供一次更新)- 但这仍然有资格跟上。你不能做的一件事就是要求证券交易所放慢速度。
这些(真实世界的)例子几乎都落在了光谱的两端,没有太多的灰色区域。但是那里有很多灰色区域,控制不明确。
有时您在 client/server 系统中戴着客户端的帽子,很容易陷入牺牲控制的陷阱,或者将控制放在错误的位置 - 这可以通过正确的设计轻松修复.考虑一下:
客户端应用程序显示来自服务器的新闻更新。
- 新闻更新随时提交到服务器并大量创建。
- 客户端应按客户端设置的时间间隔刷新。
- 刷新间隔可以随时更改,用户可以随时请求立即刷新。
- 客户端仅显示由用户指定的带有特定关键字标记的更新。
- 新闻更新有时很长,客户端不应存储新闻更新的全部内容,而是显示标题和摘要。
- 应用户要求,可以显示文章的全部内容。
在这里,新闻更新的频率不受客户端控制。但所需的刷新率和感兴趣的标签是。
让客户端接收所有到达的新闻更新并在客户端过滤它们是行不通的。但是有很多选择:
- 服务器是否应在考虑客户端刷新率的情况下发送更新数据流?客户端掉线怎么办?
- 如果有数千个客户端怎么办?如果客户端想要立即刷新怎么办?
有很多有效的方法可以解决这个问题,其中包括或多或少的反应元素。但是任何好的解决方案都应该考虑到客户端对标签和期望刷新率的控制,以及缺乏对新闻更新频率的控制(由客户端或服务器)。您可能希望服务器通过更新它推送给客户端的事件来 对客户端兴趣的变化做出 反应 - 只要客户端正在监听(通过心跳检测)它就会推送.当用户想要一篇完整的文章时,客户端会拉文章。
Rx 社区对 back-pressure 有很多争论。这是客户端应该在过载时通知服务器并且服务器通过某种方式减少事件流来响应的想法。我认为这是一种误入歧途的方法,会导致设计混乱。
在我看来,一旦客户需要提供此反馈,它就没有通过响应率测试。此时,你不是处于 reactive 的情况,你处于 async enumerable 的情况。即客户应该说ng“我准备好了”,当它准备好更多时,然后以 non-blocking 的方式等待服务器响应。
如果第一个场景被修改为文件以 drop-folder 的形式到达,并且处理的长度和复杂性各不相同,那么这将是合适的。客户端应该对下一个文件进行 non-blocking 调用,处理它,然后重复。 (根据需要添加并行性)- 不响应 file-arrived 事件流。
总结
我有意避免了其他有效的问题,例如代码的可维护性、Rx 本身的性能等。大多数是因为它们在其他地方得到了解决,更重要的是因为我认为这里的想法比那些问题更具分歧性。
因此,如果您在您的场景中反思 控制 和 响应率 的要素,您很可能会走在正确的轨道上。
响应率问题可能很微妙 - 度数 方面很重要。到达率可能会波动,并且响应率会有一定程度的可接受波动 - 显然,如果您最终没有办法“赶上”那么在某个时候客户会崩溃。
我已经在我的项目中使用 RxJava 大约一年了。 随着时间的推移,我变得非常喜欢它 - 现在我想也许太多了......
我现在写的大多数方法里面都有某种形式的 Rx,这太棒了! (直到不是)。 我现在注意到有些方法需要大量工作来组合不同的可观察生成方法。 我有一种感觉,虽然我现在理解我写的东西,但下一个程序员将很难理解我的代码。
在进入底线之前,让我直接从我的 Kotlin 代码中举一个例子(不要深入研究它):
private fun <T : Entity> getCachedEntities(
getManyFunc: () -> Observable<Timestamped<List<T>>>,
getFromNetwork: () -> Observable<ListResult<T>>,
getFunc: (String) -> Observable<Timestamped<T>>,
insertFunc: (T) -> Unit,
updateFunc: (T) -> Unit,
deleteFunc: (String) -> Unit)
= concat(
getManyFunc().filter { isNew(it.timestampMillis) }
.map { ListResult(it.value, "") },
getFromNetwork().doOnNext {
syncWithStorage(it.entities, getFunc, insertFunc, updateFunc, deleteFunc)
}).first()
.onErrorResumeNext { e -> // If a network error occurred, return the cached data and the error
concat(getManyFunc().map { ListResult(it.value, "") }, error(e))
}
简而言之,它的作用是:
- 从存储中检索一些带时间戳的数据
- 如果数据不是新的,从网络获取数据
- 再次与存储同步网络数据(以更新它)
- 如果发生网络错误,再次检索旧数据和错误
- 如果数据不是新的,从网络获取数据
我的实际问题来了:
响应式编程提供了一些非常强大的概念。但据我们所知 with great power comes great responsibility
.
我们的底线在哪里?用很棒的反应性单行代码填充我们的整个程序是否可以,还是我们应该只为真正平凡的操作保存它?
显然这是非常主观的,但我希望有更多经验的人可以分享他的知识和陷阱。 让我措辞更好
How do I design my code to be reactive yet easy to read?
我发现在编写 Rx(或任何温和的 sophisticated/new 技术)时我会记住两件事
- 我可以测试吗?
- 我可以轻松雇用可以维护它的人吗?维护不费力,一个人维护就好了?
为此,我还发现仅仅因为你可以,并不总是意味着你应该。作为指南,我尽量避免创建超过 7 行代码的查询。比这更大的查询,我尝试分成我编写的子查询。
如果您提供的代码处于代码库的核心,并且处于复杂性的极端,那么它可能没问题。但是,如果您发现所有 Rx 代码都如此复杂,您可能会创建一个难以使用的代码库。
当你拿起 Rx 时,它会变得闪闪发亮hammer,一切都开始看起来像生锈的钉子,等待你敲打。
就我个人而言,我认为最大的线索在于名称,reactive 框架。鉴于需求,您需要反思反应式解决方案是否真正有意义。
在任何 Rx 命题中,您都希望引入一个或多个事件流并执行一些响应事件的操作。
我觉得有两个关键问题要问:
- 你在控制事件流吗?
- 您必须以事件流的速率完成响应到什么程度?
如果您没有控制事件流并且您必须 以事件流的速率响应,那么 Rx 是一个很好的候选者。
在任何其他情况下,这可能是一个糟糕的选择。
我见过很多这样的例子,人们为了证明 Rx 的合理性而跳过重重障碍来制造缺乏控制的错觉——这对我来说似乎很疯狂。为什么要放弃你拥有的控制权?
一些例子:
您必须从固定的文件列表中提取数据并将其存储在数据库中。您决定将每个文件名推送到一个主题中,并创建一个反应式管道来打开每个文件并投影数据,然后以某种方式处理数据并最终将其写入数据库。
这未通过控制测试和速率测试。遍历文件并拉它们并尽可能快地处理它们会容易得多。 “决定推动”这句话是这里的赠品。
您需要显示证券交易所的股票价格。
显然这对 Rx 来说是个不错的选择。如果你跟不上总体价格的变化,那你就完蛋了。它 可能 是您合并价格的情况(也许每秒只提供一次更新)- 但这仍然有资格跟上。你不能做的一件事就是要求证券交易所放慢速度。
这些(真实世界的)例子几乎都落在了光谱的两端,没有太多的灰色区域。但是那里有很多灰色区域,控制不明确。
有时您在 client/server 系统中戴着客户端的帽子,很容易陷入牺牲控制的陷阱,或者将控制放在错误的位置 - 这可以通过正确的设计轻松修复.考虑一下:
客户端应用程序显示来自服务器的新闻更新。
- 新闻更新随时提交到服务器并大量创建。
- 客户端应按客户端设置的时间间隔刷新。
- 刷新间隔可以随时更改,用户可以随时请求立即刷新。
- 客户端仅显示由用户指定的带有特定关键字标记的更新。
- 新闻更新有时很长,客户端不应存储新闻更新的全部内容,而是显示标题和摘要。
- 应用户要求,可以显示文章的全部内容。
在这里,新闻更新的频率不受客户端控制。但所需的刷新率和感兴趣的标签是。
让客户端接收所有到达的新闻更新并在客户端过滤它们是行不通的。但是有很多选择:
- 服务器是否应在考虑客户端刷新率的情况下发送更新数据流?客户端掉线怎么办?
- 如果有数千个客户端怎么办?如果客户端想要立即刷新怎么办?
有很多有效的方法可以解决这个问题,其中包括或多或少的反应元素。但是任何好的解决方案都应该考虑到客户端对标签和期望刷新率的控制,以及缺乏对新闻更新频率的控制(由客户端或服务器)。您可能希望服务器通过更新它推送给客户端的事件来 对客户端兴趣的变化做出 反应 - 只要客户端正在监听(通过心跳检测)它就会推送.当用户想要一篇完整的文章时,客户端会拉文章。
Rx 社区对 back-pressure 有很多争论。这是客户端应该在过载时通知服务器并且服务器通过某种方式减少事件流来响应的想法。我认为这是一种误入歧途的方法,会导致设计混乱。
在我看来,一旦客户需要提供此反馈,它就没有通过响应率测试。此时,你不是处于 reactive 的情况,你处于 async enumerable 的情况。即客户应该说ng“我准备好了”,当它准备好更多时,然后以 non-blocking 的方式等待服务器响应。
如果第一个场景被修改为文件以 drop-folder 的形式到达,并且处理的长度和复杂性各不相同,那么这将是合适的。客户端应该对下一个文件进行 non-blocking 调用,处理它,然后重复。 (根据需要添加并行性)- 不响应 file-arrived 事件流。
总结
我有意避免了其他有效的问题,例如代码的可维护性、Rx 本身的性能等。大多数是因为它们在其他地方得到了解决,更重要的是因为我认为这里的想法比那些问题更具分歧性。
因此,如果您在您的场景中反思 控制 和 响应率 的要素,您很可能会走在正确的轨道上。
响应率问题可能很微妙 - 度数 方面很重要。到达率可能会波动,并且响应率会有一定程度的可接受波动 - 显然,如果您最终没有办法“赶上”那么在某个时候客户会崩溃。