ReactiveSwift over上的单向架构-第一部分:Redux

(最初在 此处 此处 发布在Sigma Software博客上

共同的可变状态是万恶之源。 ©Henrik Eichenhardt

自从Facebook在2014年推出Flux以​​来,Dan Abramov和Andrew Clark发布了它的继任者/替代Redux,因此在软件开发世界中有很多关于单向架构的炒作。 我最近发布了我在ReactiveSwift上实现Redux的初始版本。 本文简要(不是真的)总结了为什么这样做的原因,以及为什么我认为它与已经为Swift编写的其他Redux内容有所不同。

我(以及我将在下面提到的其他库)试图解决什么问题? 答案非常简单和明显-状态管理。 如今,应用程序已变得如此庞大,对可预测状态管理的需求促使人们创建了诸如Redux之类的可预测状态容器。

与服务器端解决方案不同,客户端应用程序需要另一种降低整体解决方案复杂性的方法。 无论目标是哪个平台客户端应用程序(浏览器还是移动设备),它都应该执行处理大量事件的非常复杂的任务:用户输入,平台状态更改,网络事件和更新。 通常,一个应用程序内部有很多不明显的数据流:在用户输入时触发网络请求,同时在应用程序的存储中存储一些临时状态,解析响应并将另一个写入事务写入应用程序的存储,如果用户在第一个正在进行的过程中排队另一个网络请求随着输入的改变,他的想法…针对移动设备的特定事物变得更加疯狂,因为多线程跳入了该游戏。 移动开发人员需要麻烦的是仅在主线程上进行UI更新,避免在接触数据库时并发,避免死锁等。长话短说,

原始Redux文档最终以可预测状态管理的三个原则为基础。 如果您对原始作者的详细披露感兴趣,我将给他们命名,并让您在这里阅读更多。 这些是:

  1. 真理的单一来源
  2. 状态为只读
  3. 使用纯函数进行更改

我将简要介绍实现它们的Swift端口:

ReSwift已经存在了一段时间。 它大约有5,000个github星级,并且是所有主要Swift版本的最新信息。 该库与原始JS实现紧密移植,因此增加了一些讨厌的Swift功能,例如通用状态类型,强大的订户类型等。

ReduxKit也被广泛使用,但已被ReSwift弃用。 也有一些鲜为人知的实现,例如this,this和this,但是它们只是从人们的项目中摘录而来,并没有得到很好的维护,无法用作社区验证的解决方案。

总体而言,他们建议以下应用数据流(出于显而易见的原因,称为“单向”):

在这里,我们仅看到有关上述原则实施的一些细节:

  1. Store是应用程序State 唯一真实来源
  2. 订阅Store更新时, State只读的并且可以通过视图观察
  3. State由称为Reducer的纯函数修改。 他们拥有先前的状态和视图发出的动作,从而计算出新应用的状态。 封装将减速器隐藏在视野之外,因此状态更改是隔离的。

关于为什么我仍然还是想出自己的https://github.com/soxjke/Redux-ReactiveSwift? 以下是我认为仍然更好的一些原因:

  1. 简单。 尽管ReSwift涵盖了观察,订阅/取消订阅,事件分发,线程安全,接口和协议等方面,但我的解决方案利用了ReactiveSwift的强大功能,并或多或少地由单个Store类表示。 对于我来说,上述所有东西都是开箱即用的,因为我使用了MutablePropertyStore MutableProperty
  2. 灵活性。 我的Redux实现提供了由通用StateEvent类型参数化的Store ,并为Defaultable类型提供了一些类型扩展,如果状态类型提供了默认值,则允许Store初始化而没有默认状态。 ReactiveSwift的PropertyProtocolBindingTargetProvider协议的一致性为使用简单的<~运算符绑定状态/事件流提供了可能性。
  3. 强大且易用。 借助ReactiveSwift的State SignalSignalProducer ,它可以进行简单的预订以及复杂的mapfilterreducecollect等操作,以编写具有FRP味道的真正的声明性代码
  4. State要求更严格,对外部世界的要求不严格。 “限制器”是什么意思-State或Event没有可选项,没有包装。 我所说的外部世界需求是什么-无需在State变更后添加协议一致性。 是的,没有任何具有associatedtype协议,因此可以使用依赖注入随意构建任何您喜欢的商店并创建松耦合的解决方案(作为我用于Swift的DI框架的一个很好的例子,我可以肯定地命名为Swinject)

就是这样。 由于“对外部世界没有任何要求”,因此该解决方案与模式无关,因此可以轻松地用作MVC的一部分的模型层,作为MVVM,MVP,MV-Next的一部分的模型和视图模型层-大拱门流行语。 简单的MVVM应用程序的想法(就像ReSwift示例一样)可以在自述文件中找到,我将直接介绍更复杂的示例🌤

通常将这类应用程序作为测试任务,供申请初级iOS工程师职位的候选人使用。 他们被要求使用Gismeteo(或其他天气服务)API获取当前位置的天气数据。 天气预报应存储在某些本地存储中(通常会要求使用SQLite或CoreData,但现在是2017年,应用没有像iPhone 3GS那样的内存限制,Realm等强大的替代品因此我们将不严格要求框架/实现)。

文章的下半部分主要是使用Redux和其他一些工具来构建此应用程序的教程,因此,如果您想跳过它(危险区域:您可能会错过一些有价值的见解)–这是本文下面部分的链接,以及这是所描述示例的完整源代码的链接。

如果您仍然在这里……让我们深入研究本教程!

🔨创建项目

这看起来应该非常熟悉XCode模板,让我们创建一个名为“ Simple-Weather-App”的项目,并确保选中“单元测试”复选标记。

当然,我们将CocoaPods用作依赖项管理器(因为我认为这是最好的依赖项管理器,Carthage和SPM并不像Pod那样强大而广泛)

让我们继续在根项目目录中创建Podfile。 我通常将此模板用于多目标项目,并且不反对您自己使用borrow

屏幕截图显示了表的已连接插座,数据源和委托。 我还禁用了安全区域参考线和特征变化,因为该视图旨在仅作为内容使用,并且相对于尺寸类别而言是不变的。 表格视图需要单元格来显示内容,让我们创建一个。 我们需要一个显示天气值的单元格,它可以采用以下格式(通过探索AccuWeather API获得的信息):

  • {Value} {Unit} //单行气象数据,例如气温:68 F
  • {Value}-{Value} {Unit} //数据范围,例如,预测风速10–12m / s
  • {Value} {Unit} / {Value} {Unit} //日/夜。 降水25%/ 75%

让我们继续前进,用XIB和两个标签创建单元类:名称和值。 WeatherFeatureLabel.swiftWeatherFeatureLabel.xib ,我们开始:

接下来,我们将在存根实现中添加到WeatherView以在屏幕上添加表格视图单元和WeatherView

点击“运行”,您将可以在Simulator上看到以下UI存根

我们已经确认我们有非常基本的UI来显示我们想要显示的内容,现在让我们向项目中添加一些“内容”。 您可以在这里浏览中间结果

💃型号

让我们设置一些我们希望通过模型层实现的目标:

  • 它应该是JSON可解析/可序列化的
  • 它应该是不变的(当然!)
  • 应该很容易在UI上显示
  • 它应该代表我们的实际领域(天气)

考虑到这一点,让我们直接进行建模。 我们将创建Weather.swift并编写一些简洁(我希望如此)的代码:

连接插座并不有趣,让我们关注ViewController.setupObserving 。 通过向属性分配属性/操作,此方法使所有UI控件“处于活动状态”。

我们可以看到状态正在精确地处理-当前,预测,根据当前页面在工具栏中向左/向右命中的能力。 该应用程序完全建立在两个商店(AppStore和UIStore)及其状态组合上。 因此,粗略结构可以通过以下图表进行描述:

如我们所见,应用程序是由几个简单的职责构建而成的:

  1. 商店负责管理状态
  2. 服务是传递内容的助手类。 他们直接沟通只与商店
  3. ViewModel让UIStore负责管理UI状态。 它不应与应用程序的状态混合在一起。
  4. ViewModel本身将转换应用于应用程序状态,以使其可以在UI中使用。 它提供负责将事件传递回商店的操作。
  5. ViewController将ViewModel的动作和状态信号绑定到视图。

在此图表上,我们看到以下数据流:

  • 状态(及其转换)从下到上传播
  • 事件从上到下传播

他们用单个数据流建立了一个无限循环,这就是为什么这种架构可以称为单向的原因

changes对变化的响应

当我们谈论架构时,我们通常根据一个简单的标准来判断它们-对现成的解决方案进行更改有多么容易。 让我们快速看一下一些可能的更改:

  1. 使地理定位的超时时间不是5分钟,而是10秒-很容易(Reducer中有1行)

2.通过滑动/滚动而不是(好的,不仅如此)通过工具栏按钮在预测日之间导航-简单但不容易。 我们需要重新考虑要显示的UI层,因为一个表视图不足以提供流畅的滚动体验。 关键的复杂性是UI层-我们必须重新考虑ViewModel代码以追加其他UI动作,将内容状态传递更改为ViewController并实现响应式UIScrollView行为。

解决方案在这里:https://github.com/soxjke/Redux-ReactiveSwift/tree/ade4fd5839a3c9d9affab522838f043484036544544/Example/Simple-Weather-App。 即使查看更改量,此更改也有些复杂:

3.集成每小时预报API:https://developer.accuweather.com/accuweather-forecast-api/apis

当涉及到更改状态时,Redux变得很痛苦,因为要更改的内容太多了-状态,模型,化简器,视图模型(对于MVVM),ViewController。

🎁奖金测试

项目中还需要测试什么? 让我们来看一下ViewModelSpec 。 您可以在此处找到完整的脚本,我将在总体上介绍最重要的部分:

  • 我们需要存根以获取success状态(L16-L49)。 它们将用于设置存储以测试success逻辑。
  • 我们只需要很少的Equatable扩展(L221–227)。 他们将需要使便利的equal匹配者。
  • 我们将介绍ViewModel职责的几个主要部分-测试控件启用的状态产生器(L53-L99),测试UIStore ,由于Redux Store的特性(L100-L127),这是最简单的部分;测试动作(L128-L164) ),测试其余的SignalProducer (L165-L197)。

对反应性测试的全面描述不在本文讨论范围之内,但是您可以学到一些想法并建立自己的测试策略。

han谢谢以及下一步要去哪里

感谢您在这里阅读,我希望您喜欢并感受到如何使用单向数据流来增强您的项目能力。

我们提供了非常基本的Redux应用程序,但是它涵盖了网络,加载状态,状态持久性和还原等方面-在iOS开发中经常被忽略的精简版。 尽管Redux总体上不错,但是请记住,在软件开发世界中没有“魔法石”或“银弹”。 它甚至都不假装是-但是,如果您在状态管理和相关的bug方面苦苦挣扎,可能是时候从这里获得启发。 我将链接到Redux原始作者Dan Abramov的一篇不错的免责声明帖子:您可能不需要Redux。

另外,请不要将此示例项目视为正确执行方法的“圣经”。 毕竟,那只是我的愿景。 我有意简化了该项目,将重点放在Redux本身上,尤其是:

  • 在现实世界的项目中,我将使用幻像类型作为单元格标识符的概念,将一些工作移至UIView / UITableViewCell扩展以保持更清晰,更坚固
  • 我有解析键路径而不是文字的常量
  • 我将在代码中创建视图布局和设置,因为它具有与使用IB时不同的灵活性。 SnapKit是出色的DSL,使这一切成为可能
  • 我会使用称为“依赖注入”的概念来避免对网络和商店的依赖。 Swinject是我要用的。
  • 我将使用没有主机应用程序目标的单元测试。 这需要更复杂的隔离和模块化,但是在速度和扩展方面会获得更好的结果。 这里描述了这个想法
  • 我会提供稍微更好的测试范围-尤其是特定的错误检查(在这里快速输入错误非常方便)

🔗友情链接

示例项目在这里:https://github.com/soxjke/Redux-ReactiveSwift/tree/master/Example/Simple-Weather-App

lib本身在这里:https://github.com/soxjke/Redux-ReactiveSwift

我建议阅读整个http://redux.js.org并在这里熟悉反应式编程的概念。

另外,Podfile中所有使用的库都经过了实战测试,如果您不使用某些东西,那么是个开始的好时机

🔑关键见解

  • 在您的应用中,尝试受益于不可变和只读状态。
  • 纯功能易于测试。 有状态的实例很难测试。
  • 纯功能可以安全地公开/内部使用
  • Redux既不是最短也不是最简单的方法。 但这是可测且可预测的