作为一个有12年Android开发经验, 3年React Native经验的前端开发, 我想聊聊最近大家都在讨论的MVI模式, 聊聊他们的不同与好处及弊端.
Redux
React界的Redux架构, 本身来自Facebook的Flux架构. 它的主要思想是:
graph TD
View -->|action|Reducer
Reducer -->|calculate|Store
Store -->|interested?|View
以前写页面的方法
以前我们写数据是写在Activity或Fragment中, 或者好一点的写在Presenter或ViewModel里. 举个例子, 纯伪代码哦, 就是一个页面只有一个textView显示有多少条todo项, 一个button来添加条todo待办项. 为了简单说明,也没有ViewModel出现.
class MyPage { //伪代码, 可理解为Activity
val data: TodoList
fun render() { //伪代码, 可理解为onCreate()
tv.text = data.count
btn.click {
data.add("***")
}
}
}
介绍Store与State
但现在Redux要求数据要写到Store中来. Store本身可以理解成一个大的HashMap, 它里面放满了所有你页面所需要的数据. 假设你有多个页面, 每个页面的数据分别是TodoList, ProductList, ..., 那你的Store就是类似一个JSON对象, 或一个HashMap:
{
"todos": [ {todo}, {todo}, ...],
"products": [ {product}, {product}, ... ],
}
所以你的Activity中就不能有data数据了, 它应该放到Store里去. 即:
// connect()方法来自于Redux库, 即把Page这个View与Store给联系起来. 这就相当于是一个高阶函数, connect(view, precondition)仍再返回一个View出来
// _MyPage就是被包裹的View, 即要画的页面
// 第二参是一个lambda, 是说明我这个数据只关心State中哪个数据.
// Store中数据就叫State. 要是State的其它子部分, 如products更新了, 我这页并不关心, 也不刷新;
// 要是我关心的todos刷新了, 那我这个_MyPage就要刷新.
val MyPage = connect(_MyPage, {state -> state.todos} )
// 名字变了, 变成一个private类. 但内容仍是复杂渲染
class _MyPage {
// val data: TodoList //不能自己带数据了
val data = getDataFromStore(); // getDataFromStore()来自redux (伪代码), 上面的第二参会让这里返回个state.todos
fun render() { //伪代码, 可理解为onCreate()
tv.text = data.count
btn.click {
data.add("***")
}
}
}
介绍action
上面的代码, 让我们的view彻底变成个passive view, 只接收数据, 然后渲染, 并不会自己维护数据.
但仍有一点不好的那就是,data.add("..")这里直接去改了数据. Flux/Redux中认为Store中的数据(State)应该是immutable的, 即不能变更 -- 这一点就像是kotlin中的List, 而不是kotlin中的MutableList.
要是你真想添加一条todo事项, 那就得提供一个全新state数据, 而不是在旧有state数据上修改. 而且这种变化应该让专业人士来干, View只负责渲染就不要干了, 所以View应该发出一个action(你可以理解为EventBus中的event), 然后专业人士, 叫Reducer, 会接收来做数据的变化.
所以原来btn.click中直接变化 data.add("") 就要改成:
fun ACTION_ADD_TODO(title, despcription) = Action(
type: "ACTION_ADD_TODO",
data: {title, description}
) //即返回一个Action对象, 内有type与data
btn.click {
// data.add("***")
dispatch(ACTION_ADD_TODO("", "") // dispatch()方法来自于redux, 即发现了个action
}
然后Reducer来操作数据. 注意, reducer要求Store中的state是一个Immutable对象, 所以你不能直接对它进行加减修改, 而是先clone它再对clone体做操作, 再返回这个新state(即clone体)
// 第一参就是当前store里的数据state
// 第二参则来自于你dispatch了什么action, 每次dispatch, 这个reducer都会被调用的
fun reducer(state, action) {
when(action.type) {
"ACTION_ADD_TODO" -> {
val newTodos = newListFrom(state.todos)
newTodos.add(action.data)
val newState = state.clone()
newState.todos = newTodos;
return newState;
}
}
}
注意, Reducer自己是一个pure function, 即入参决定了返回值, 没有外部影响, 即没有side effect.
串联起View, Reducer, Store来
现在我们的app首页是一个Router树, 它包含了app中所有页面, 类似Android中的Manifest:
val store = createStore(reducer)
val app =
<Root store = {store}>
<MyPage/>
<Page2/>
<page3/>
...
</Root>
这样一来,
- view中每dispatch一个action, 都会触发reducer.
- Reducer计算出一个新state, 然后写入到store里
- store里一有更新, 就会更新Router树中的所有页面, 问他们"state有更新了, 若是你感兴趣的数据变化了, 请你刷新一下"
Redux的弊端
1. 不友好
如果你是一个Android程序员, 对上面的讲解有点晕, 没毛病. 是的, Redux的最大问题就是:对程序员不友好
我个人是很讨厌一些对程序有好处, 但对程序员很不友好的库或工具的, 如Dagger, Redux. 你为了修改一个数据, 你要做的事是:
- 1). 新加一个Action类型
- 2). 在reducers中新加一个if-else来处理这种action
而当你要新加一个页面, 你要修改的东西就更多了:
- 1). 新加一个页面
- 2). 把页面给connect()包装一层
- 3). 页面注册到首页的router树上
- 4). 新加你这个页面要用的ACTION (可能有多个)
- 5). 在reducer中对这些新ACTION们进行一一处理
这还只是新加. 要是你过了半年再回头看代码, 或是看他人写的代码, 因为redux中有reducer, action, store各种分隔了代码, 甚至reducer自己还有我们上面没介绍的middleware, 于是你得到处找, 真正处理这数据的代码到底在哪里. 很心累!
总结: 写起来, 读起来, 都累
2. 对"single source of truth"的理解有偏差
我不反对"single source of truth"的原则. Redux也是基于此, 做了一个Store, 把app的数据全放到了Store中. 但这样引发了几个问题:
1). 全app的数据都在Store里, 这个State也太大了
2). Page1其实根本不关心Page2的变化, 但每次store更新, 都会去问所有页面, 包括page1, "数据有更新, 你要更新吗?". 这些都是多余的, 浪费的操作.
3). 每次新加action, 都要去reducer中新加一个if-else, 这明显违背了OCP(开放-关闭原则), 容易出错, 不利于扩展.
我个人觉得, 只要你的数据没有dulication, 这就是single source of truth. 没必要把全app的数据都存到一个类里
着迷于"单向数据传输"
这个也是很多文章中说的Redux的好处.
首先, 我们的MVP, 特别是MVVP, 本身 就是单向数据变化的. View自己只是个passive view. 并不是Redux才是单向. 另外, "单向数据传输"并不是个宝贝, 就Android中一个页面的体量, 我觉得只要View与Presenter/ViewModel分开, 没有重复数据, 这就已经很方便我们做单元测试, 也帮我们做了解耦合. 我就很满意了. 单向不单向, 我个人并不是很在意.
MVI
好了, 讲完了Redux, 现在再回来看MVI, 是不是有Redux那味了 Intent其实就是dispatch action; 数据也是存到了Store里的State里.
因为个人觉得MVI和Redux太像了, 所以我其实没有太深入钻研它. 但通过上面讲解Redux的弊端, 我想要是真的去应用MVI到项目中的话, 也应该避免上面的问题
-
- 对程序员不友好. 我们的代码应该好扩展, 好写, 也更容易读.
-
- app中所有数据存于一个类中. 这个问题很大, 有违反OCP的问题, 有性能的问题. 我还是建议要是真用MVI, 每个页面自己维护自己的数据就好了.
-
- 着迷于单向数据传输. 我觉得只要好读, 好写, 方便以后扩展, 能DI(方便做单元测试), 我就已经很满意了.
结语
总之一句话就是: 选择适合你的架构.
不限于MV*什么方式, 只要view, logic分离, 只要没有duplication, 只要方便单元测试(这一般也能保证你的架构能方便地扩展), 这三原则满足了, 这就是个好架构.
所以我其实最喜欢的就是MVP架构. 最简单, 很明显, 读起来不用跳来跳去. 使用起来也简单, 单元测试也方便. 不过现在主流还是MVVM啦.
个人一点拙见, 仅供诸位参考~