前言
在开始之前先介绍下纯函数,附加作用相关概念。
纯函数
什么是纯函数?
在函数式编程里我们会经常谈到这两个概念。一个是纯函数。另一个是 附加作用(副作用)。这里先就结合实际来介绍一下 纯函数。
下面给出两个函数 increaseA
和 increaseB
,increaseB
是 纯函数,increaseA
不是 纯函数。
var state = 0
func increaseA() {
state += 1
}
increaseA()
print(state) // 结果: 1
func increaseB(state: Int) -> Int {
return state + 1
}
let state = increaseB(state: 0)
print(state) // 结果: 1
为什么 increaseB
是 纯函数?
因为他特别 纯洁:除了用入参 state 计算返回值以外没做任何其他的事情。
那为什么 increaseA
不是 纯函数?
因为他修改了函数本体以外的值 state
, 他拥有这个 附加作用。
那么纯函数有什么作用呢?看看下面两个测试例子,猜测他们能不能测试成功:
func testIncreaseA() {
increaseA()
state == 1 // 结果:?? 🤔
}
func testIncreaseB() {
let state = increaseB(state: 0)
state == 1 // 结果:true 😎
}
可以肯定第二个测试 testIncreaseB 会成功。0 + 1 肯定等于 1。那第一个测试呢?这可不好说了,我们并不知道 increaseA 是在什么环境下被调用的,不知道在这个环境下初始 state 是多少。如果他是 0 那测试就会成功的,如果他不是 0 那测试就会失败的。因此在不知道所处环境时,我们无法判断测试是否会成功。
那么什么是附加作用呢?
附加作用
由于 increaseA
存在修改外部 state
的 附加作用所以他不是 纯函数。事实上如果函数有以下任意一种作用,他也不是纯函数:
- 发起网络请求
- 刷新 UI
- 读写数据库
- 获取位置信息
- 使用蓝牙模块
- 打印输出
- ...
我们将这些作用称为函数的 附加作用(副作用)。
而纯函数的定义就是: 没有附加作用的函数,并且在参数相同时,返回值也一定相同。
因此在已知执行逻辑时,纯函数所产生的结果是可以被预测的。一些现代化的库都利用了这个特性来做状态管理,如:RxFeedback, Redux,ReactorKit 等等
纯函数用于状态管理
用一个足够简单的例子来演示,如何用 纯函数做状态管理:
typealias State = Int
enum Event {
case increase
case decrease
}
func reduce(_ state: State, event: Event) -> State {
switch event {
case .increase:
return state + 1
case .decrease:
return state - 1
}
}
这个例子似乎过于简单,以至于看不出有什么特别的。好吧,我承认他的主要目的是向大家演示,用 纯函数 做状态管理的基本单元是什么。
首先,得有个状态:
typealias State = Int
然后,要有各种事件:
enum Event {
case increase
case decrease
}
这样,就可以做测试了,当 App 处于某个状态时,发生了某个事件,会产生一个结果,这个结果是否符合预期:
func testReduce() {
let state1 = reduce(0, event: .increase)
state1 == 1 // 结果:true 😎
let state2 = reduce(10, event: .decrease)
state2 == 9 // 结果:true 😎
}
以上两个测试都是成功的。当然这里的状态管理过于简单。而真实应用程序的状态都是非常复杂的。并且程序的行为都是很难预测的。要解决这个问题,要感谢纯函数,还记得他的特征吗?
纯函数在参数相同时,返回值也一定相同。
再来看下 reduce
方法:
func reduce(_ state: State, event: Event) -> State { ... }
有没有获得一点点灵感...
...
...
...
...
...
... 经过 60 秒后
希望你已经获得答案了。
现在,再来看一个相对复杂的例子:
登录状态管理
typealias UserID = String
enum LoginError: Error, Equatable {
case usernamePasswordMismatch
case offline
}
struct State: Equatable {
var username: String
var password: String
var loading: Bool
var data: UserID?
var error: LoginError?
enum Event {
case onUpateUsername(String)
case onUpatePassword(String)
case onTriggerLogin
case onLoginSuccess(UserID)
case onLoginError(LoginError)
}
static func reduce(_ state: State, event: Event) -> State {
var newState = state
switch event {
case .onUpateUsername(let username):
newState.username = username
case .onUpatePassword(let password):
newState.password = password
case .onTriggerLogin:
newState.loading = true
newState.data = nil
newState.error = nil
case .onLoginSuccess(let userId):
newState.loading = false
newState.data = userId
case .onLoginError(let error):
newState.loading = false
newState.error = error
}
return newState
}
}
重新走下流程 😄,用 纯函数 做状态管理:
首先,得有个状态:
struct State: Equatable {
var username: String // 输入的用户名
var password: String // 输入的密码
var loading: Bool // 登录中
var data: UserID? // 登录成功
var error: LoginError? // 登录失败
...
}
然后,要有各种事件:
enum Event {
case onUpateUsername(String) // 更新用户名
case onUpatePassword(String) // 更新密码
case onTriggerLogin // 触发登录
case onLoginSuccess(UserID) // 登录成功
case onLoginError(LoginError) // 登录失败
}
最后,我们要有一个 纯函数来管理状态:
static func reduce(_ state: State, event: Event) -> State {
var newState = state
switch event {
case .onUpateUsername(let username):
newState.username = username
case .onUpatePassword(let password):
newState.password = password
case .onTriggerLogin:
newState.loading = true
newState.data = nil
newState.error = nil
case .onLoginSuccess(let userId):
newState.loading = false
newState.data = userId
case .onLoginError(let error):
newState.loading = false
newState.error = error
}
return newState
}
现在可以在单元测试环境中模拟各种事件,并且判断结果是否符合预期:
- 更新用户名事件
func testOnUpateUsername() {
let state = State(
username: "",
password: "",
loading: false,
data: nil,
error: nil
)
let newState = State.reduce(state, event: .onUpateUsername("beeth0ven"))
let expect = State(
username: "beeth0ven",
password: "",
loading: false,
data: nil,
error: nil
)
newState == expect // 结果:true 😎
}
- 更新密码事件
func testOnUpatePassword() {
let state = State(
username: "beeth0ven",
password: "",
loading: false,
data: nil,
error: nil
)
let newState = State.reduce(state, event: .onUpatePassword("123456"))
let expect = State(
username: "beeth0ven",
password: "123456",
loading: false,
data: nil,
error: nil
)
newState == expect // 结果:true 😎
}
- 触发登录事件
func testOnTriggerLogin() {
let state = State(
username: "beeth0ven",
password: "123456",
loading: false,
data: nil,
error: nil
)
let newState = State.reduce(state, event: .onTriggerLogin)
let expect = State(
username: "beeth0ven",
password: "123456",
loading: true,
data: nil,
error: nil
)
newState == expect // 结果:true 😎
}
- 登录成功事件
func testOnLoginSuccess() {
let state = State(
username: "beeth0ven",
password: "123456",
loading: true,
data: nil,
error: nil
)
let newState = State.reduce(state, event: .onLoginSuccess("userID007"))
let expect = State(
username: "beeth0ven",
password: "123456",
loading: false,
data: "userID007",
error: nil
)
newState == expect // 结果:true 😎
}
- 登录失败事件
func testOnLoginError() {
let state = State(
username: "beeth0ven",
password: "123456",
loading: true,
data: nil,
error: nil
)
let newState = State.reduce(state, event: .onLoginError(.usernamePasswordMismatch))
let expect = State(
username: "beeth0ven",
password: "123456",
loading: false,
data: nil,
error: .usernamePasswordMismatch
)
newState == expect // 结果:true 😎
}
现在,知道了如何用纯函数做状态管理了。不过当前的代码形态,离投入生产环境,还存在好几个过度形态。这些过度形态有的是围绕如何引入附加作用,而做了一些应用架构。在这个问题上,不同地架构也提出了不同的解决方案,后面会详细介绍ReactorKit结合了纯函数,附加作用和RxSwift响应编程的单向数据流架构。
ReactorKit
介绍
ReactorKit 结合了纯函数,附加作用和响应式编程,通过模板设计模式实现的单向数据流架构框架。用户行为和页面状态都是通过序列相互传递。这些序列都是单向的:页面只能发出用户行为,然而反应器(Reactor)只能发出状态。
View
View 用于展示数据。ViewController 和 Cell 都可以看作是 View。View 将用户输入绑定到 Action 的序列上,同时将页面状态绑定到 UI 组件上。
定义一个 View 只需要让它遵循 View 协议即可。然后你的类将自动获得一个 reactor 属性。这个属性应该在 View 的外面被设置或者页面初始化的时候设置:
class ProfileViewController: UIViewController, View {
var disposeBag = DisposeBag()
}
profileViewController.reactor = UserViewReactor() // 注入 reactor
当 reactor 属性被设置时,bind(reactor:) 方法就会被调用。执行这个方法来进行用户输入绑定和状态输出绑定。
func bind(reactor: ProfileViewReactor) {
// action (View -> Reactor)
refreshButton.rx.tap.map { Reactor.Action.refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
// state (Reactor -> View)
reactor.state.map { $0.isFollowing }
.bind(to: followButton.rx.isSelected)
.disposed(by: self.disposeBag)
}
Reactor
Reactor 是与 UI 相互独立的一层,主要负责状态管理。Reactor 最重要的作用就是将业务逻辑从 View 中抽离。每一个 View 都有对应的 Reactor 并且将所有的逻辑代理给 Reactor。Reactor 不需要依赖 View,所以它很容易被测试。
遵循 Reactor 协议即可定义一个 Reactor。这个协议需要定义三个类型:Action,Mutation 和 State。它也需要一个 initialState 属性。
class ProfileViewReactor: Reactor {
// 代表用户行为
enum Action {
case refreshFollowingStatus(Int)
case follow(Int)
}
// 代表附加作用
enum Mutation {
case setFollowing(Bool)
}
// 代表页面状态
struct State {
var isFollowing: Bool = false
}
let initialState: State = State()
}
Action 代表用户行为,State 代表页面状态。Mutation 是 Action 和 State 的桥梁。Reactor 通过两步将用户行为序列转换为页面状态序列:mutate() 和 reduce()。
mutate()
mutate() 接收一个 Action ,然后创建一个 Observable<Mutation>。
func mutate(action: Action) -> Observable<Mutation>
每种附加作用,如,异步操作,API 调用都是在这个方法内执行。
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .refreshFollowingStatus(userID): // receive an action
return UserAPI.isFollowing(userID) // create an API stream
.map { (isFollowing: Bool) -> Mutation in
return Mutation.setFollowing(isFollowing) // convert to Mutation stream
}
case let .follow(userID):
return UserAPI.follow()
.map { _ -> Mutation in
return Mutation.setFollowing(true)
}
}
}
reduce()
reduce() 通过旧的 State 以及 Mutation 创建一个新的 State。
func reduce(state: State, mutation: Mutation) -> State
这个方法是一个纯函数。它将同步的返回一个 State。不会产生其他的作用
func reduce(state: State, mutation: Mutation) -> State {
var state = state // create a copy of the old state
switch mutation {
case let .setFollowing(isFollowing):
state.isFollowing = isFollowing // manipulate the state, creating a new state
return state // return the new state
}
}
transform()
transform() 转换每一种序列。有三种转换方法:
func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
执行这些方法可以转换或者组合其他的序列。例如,transform(mutation:) 最适合用来组合一个全局事件,生成一个 Mutation 序列。
它也可用来做调试:
func transform(action: Observable<Action>) -> Observable<Action> {
return action.debug("action") // Use RxSwift's debug() operator
}
优缺点总结
统一管理状态变量有以下几个优点:
- 结构清晰:统一的模板,ERP统一化;
- 逻辑清晰:在浏览页面的代码时只要查看这个类型就能知道哪些变量是需要特别关注的;
- 页面持久化监控:只需序列化State这个结构体就能够保存这个页面的全部信息,在恢复时只需要反序列化State,可用于重点页面的历史记录监控
缺点:
- 模板代码较多,即便是修改一个状态变量这样简单的操作都需要经过一套标准流程;
- 更高的学习成本