RxReduce 0.10

RxReduce 0.10

Thibault WittembergRxSwift Community 维护。



 
依赖项
RxSwift>= 4.5.0
RxCocoa>= 4.5.0
 


  • 作者:
  • Thibault Wittemberg 和 RxSwiftCommunity
RxReduce Logo
Travis CI Build Status
框架 Carthage Compatible CocoaPods Compatible
平台 Platform
许可证 License

关于

RxReduce 是状态容器模式(如 Redux)的响应式实现。它基于状态不可变和单向数据流的概念。

架构问题

这几年来,关于将其他架构模式应用于移动应用的博客文章、教程、书籍、研讨会非常多。所有这些模式背后的理念都是为了提供更好的方法来

  • 满足 SOLID 要求(维基百科
  • 通过设计来产生更安全的代码
  • 让我们的代码更容易测试

古老的老式 MVC 越来越被 MVP、MVVM 或 VIPER 取代。这里我不会详细介绍这些模式,因为它们已经得到了很好的文档记录。我认为 MVVM 目前是最流行的模式,这主要是因为它与 MVC 和 MVP 的相似性,以及它利用数据绑定来简化数据流程的能力。此外,它很容易通过 Coordinator 模式和响应式编程来增强。

如果您对响应式 Coordinators 感兴趣,可以查看这个项目(RxFlow👌

话虽如此,至少还有另一种架构模式脱颖而出:状态容器

其中一个最著名的例子是 Redux,但让我们不要局限于特定的实现。

关于状态容器的一些资源:

该模式的主要目标是

  • 在您的应用程序中展示清晰/可重复的数据流
  • 依赖于单一事实来源:即 状态
  • 利用值类型来处理状态的不可变性
  • 推广函数式编程,因为唯一改变状态的方式是应用一个自由函数:reducer(还原器)

相较于更传统的做法,我认为这种方法非常有趣,因为它负责维护应用状态的一致性。MVC,MVP,MVVM或VIPER可以帮助将你的应用程序切片成定义良好的层,但它们在处理应用程序状态方面并没有那么指导。

响应式编程是状态容器架构的绝佳伴侣,因为它可以帮助

  • 传播状态修改
  • 构建异步操作以修改状态(用于网络、持久性等)

RxReduce

RxReduce

  • 提供了一個通用的存储库,可以处理各种类型的状态
  • 通过响应式机制公开状态修改
  • 提供了一个简单/统一的通过动作(Actions)以同步和异步方式修改状态的方法

安装

使用Carthage

在你的Cartfile文件中

github "RxSwiftCommunity/RxReduce"

使用CocoaPods

在你的Podfile文件中

pod 'RxReduce'

关键原则

RxReduce的核心机制非常直观

以下是一个动画,解释了状态容器架构内的流程

StateContainerArchitectureFlow
  • Store(存储库)是处理你状态的组件。它只有一个输入:一个函数“dispatch()”,它接收一个参数:一个动作(Action)。
  • 唯一触发状态修改的方式就是调用这个“dispatch()”函数。
  • 动作(Actions)是没有业务逻辑的简单类型。它们封装了修改状态所必需的有效负载。
  • 只有称为 Reducer (RxReduce !)的免费和可测试的函数才能改变 State。一个 “reduce()” 函数接受一个 State,一个 Action,并返回一个改变的 State... 这就这么简单。更精确地说,一个 reducer 返回 State 的一个改变过的子状态。实际上,每个 State 的子状态都有一个 reducer。我们所说的子状态指的是组成 State 的所有属性。
  • Store 将确保您针对每个子状态只提供一个且只有一个 reducer。这为您应用逻辑带来了安全性和一致性。每个 reducer 都有一个明确的范围。
  • Reducers 不能 执行异步逻辑,它们只能以同步和可读的方式改变状态。异步作业将由 Reactive Actions 处理。
  • 您可以通过 Store 提供的 Observable<State> 接收状态变化的通知。

如何使用 RxReduce

代码示例

如何声明 State

由于状态容器的核心思想是关于不可变性,避免引用类型的不受控传播和竞争条件,因此 State 必须是值类型。结构体和枚举非常适合此用途。

struct TestState: Equatable {
    var counterState: CounterState
    var userState: UserState
}

enum CounterState: Equatable {
    case empty
    case increasing (Int)
    case decreasing (Int)
}

enum UserState: Equatable {
    case loggedIn (name: String)
    case loggedOut
}

使状态 Equatable 不是强制性的,但这将允许 Store 在两次动作之间没有变化时不要发射新的状态值。因此,我强烈建议遵循 Equatable,以最小化视图刷新的次数。

如何声明 Actions

动作是简单的数据类型,它封装了在 reducers 中用于改变状态的负载。

enum AppAction: Action {
    case increase(increment: Int)
    case decrease(decrement: Int)
    case logUser(user: String)
    case clear
}

如何声明 Reducer

正如我说的,Reducer 是一个免费函数。这类函数接受一个值,返回一个幂等值,并且不会执行任何副作用。它们的声明与类型定义无关。这非常方便进行测试👍

这里我们定义了两个 Reducer,将处理它们各自专属的子状态。第一个修改 CounterState,第二个修改 UserState。

func counterReduce (state: TestState, action: Action) -> CounterState {

    guard let action = action as? AppAction else { return state.counterState }

    var currentCounter = 0

    // we extract the current counter value from the current state
    switch state.counterState {
    case .decreasing(let counter), .increasing(let counter):
        currentCounter = counter
    default:
        currentCounter = 0
    }

    // according to the action we mutate the counter state
    switch action {
    case .increase(let increment):
        return .increasing(currentCounter+increment)
    case .decrease(let decrement):
        return .decreasing(currentCounter-decrement)
    case .clear:
        return .empty
    default:
        return state.counterState
    }
}

func userReduce (state: TestState, action: Action) -> UserState {

    guard let action = action as? AppAction else { return state.userState }

    // according to the action we mutate the users state
    switch action {
    case .logUser(let user):
        return .loggedIn(name: user)
    case .clear:
        return .loggedOut
    default:
        return state.userState
    }
}

这些 Reducer 将只处理它们负责的 Action,不多也不少。

如何声明一个 Store

RxReduce 提供了一个通用的 Store,可以处理应用程序的状态。您只需提供一个初始状态

let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))

如何合并子状态突变到一个整体状态

正如我们所见:一个 Reducer 只负责其专属的子状态。然后,我们需要定义一系列 Reducer 来处理整个应用程序的状态突变。所以,我们需要一种机制将所有突变子状态组装成一个一致的状态。

我们将使用函数式编程技术来实现这一点。

透镜

透镜是函数式编程中访问和修改值类型的一种通用方式。这是告诉 Store 如何修改某个子状态的操作。例如,CounterState 的透镜将是

let counterLens = Lens<TestState, CounterState> (get: { testState in return testState.counterState },
                                                 set: { (testState, counterState) -> TestState in
	var mutableTestState = testState
	mutableTestState.counterState = counterState
	return mutableTestState
    })

这是定义如何访问状态中的 CounterState 属性(get 闭包)以及如何修改它(set 闭包)。

变异者

变异者简单地说是将一个Reducer和子状态的一个Lens分组在一起的结构。对于CounterState来说也是一样。

let counterMutator = Mutator<TestState, CounterState>(lens: counterLens, reducer: counterReduce)

变异者包含了所有关于如何变异CounterState和如何将其设置为父状态所需的一切信息。

让我们将所有部件组合在一起

实例化商店之后,您必须注册所有将处理状态子状态的变异者。

let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))
let counterMutator = Mutator<TestState, CounterState>(lens: counterLens, reducer: counterReduce)
let userMutator = Mutator<TestState, UserState>(lens: userLens, reducer: userReduce)

store.register(mutator: counterMutator)
store.register(mutator: userMutator)

现在让我们变异状态

store.dispatch(action: AppAction.increase(increment: 10)).subscribe(onNext: { testState in
	print ("New State \(testState)")
}).disposed(by: self.disposeBag)

等等,还有更多的东西 …

动作列表

最近,Swift 4.1 引入了条件一致性。如果您对这个概念不熟悉:条件一致性的概述

基本上,它允许仅在关联的内部类型也符合该协议的情况下,使泛型类型符合协议。

例如,RxReduce利用这个特性来使动作数组成为动作!这样做之后,向商店发送动作列表就像这样发送是完全可行的

let actions: [Action] = [AppAction.increase(increment: 10), AppAction.decrease(decrement: 5)]
store.dispatch(action: actions).subscribe ...

数组中声明的动作将按顺序执行👌.

异步性

将动作数组成为动作本身是巧妙的,但是因为我们在使用响应式编程,RxReduxe也应用了这种技术到 RxSwift Observables。它提供了一种非常优雅的方式来向商店发送 >(因为 Observable 也符合 Action),使异步动作变得非常简单。

let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }
store.dispatch(action: increaseAction).subscribe ...

这将每1秒触发一个 AppAction.increase 动作,并相应地变异状态。

如果您想将RxReduce与Redux进行比较,这种执行异步操作的能力与“Action Creator”概念相当。

记录在案,我们甚至可以将一个包含Observable的数组分派到Store中,它也会被视为一个动作。

let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }
let decreaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.decrease(decrement: 1) }
let asyncActions: [Action] = [increaseAction, decreaseAction]
store.dispatch(action: asyncActions).subscribe ...

条件合规性是一个非常强大的特性。

还有一件事

Store提供了一种从任何地方“观察”状态变更的方法。您只需要订阅“state”属性即可。

store.state.subscribe(onNext: { appState in
	print (appState)
}).disposed(by: self.disposeBag)

演示应用

提供了一个演示应用来展示核心机制,例如异步性、子状态以及视图状态渲染。

Demo Application Demo Application

工具和依赖

RxReduce依赖于

  • SwiftLint进行静态代码分析(《https://github.com/realm/SwiftLint》)
  • RxSwift将状态和操作作为Observables暴露给您的应用程序和Store,以便它们可以做出反应(《https://github.com/ReactiveX/RxSwift》)
  • 在演示应用程序中使用Reusable以简化Storyboard分割到原子ViewController(《https://github.com/AliSoftware/Reusable》)