为何选择 Flux
设计上遇到的问题
最初在接触 Flux 时就有一种惊艳的感觉,长久以来在设计上所出现的困扰似乎出现了曙光。在 Flux 还没有出现之前,MVx 系列 (MVC、MVP、MVVM) 的 Design Pattern 就一直引领风潮。这类型的 Design Pattern 成功地解决了特定的问题,但却也形成了某些尾大不掉的隐忧。在画面不多、显示信息单纯的应用程序中问题不容易显现,但随着程序复杂度的升高,设计上所隐含的矛盾也不住地增强。
MVx 系列的设计在概念上是一个画面对应一种数据类型,画面专责显示与处理该类型的数据。很直觉、也很有效地把功能区分成一组、一组的单元。水能载舟亦能覆舟,正所谓成也萧何、败也萧何。就是因为每一组 MVx 太过独立、区隔性太强,当出现整合式画面的需求时,会造成在设计上进退两难的抉择。
假设程序中有一个画面叫 Dashboard,需要整合客户、订单、存货的数据。试问,这时是要设计一个新的 Model 纳入所有数据?还是打破规则让一个 View 对应多个 Model?
有人也许会问:这是问题吗?
如果只是期望程序能够运行,那的确算不上是个问题。但是如果要考虑到源代码的可维护性,就必须要维持在设计上的一致性,这点在程序愈复杂的情况下愈显重要。否则就不需要搞什么 Design Pattern,就随性而为、让一切都归于浑沌就好了。
再举另一个例子,假设要开发的是线上购物的订单画面,下单时要提供客户数据、订单数据、刷卡数据。依据之前的原则,所有的数据都会被设计纳在一个单一的 Model 内。当某一天高层突然下指令要把购物流程改成 Wizard 的方式,每个步骤各自独立成一个画面。试问在这样的情况下,开发新画面时是让 Model 拆解成多个?还是维持原本的样子?
如果要维持原本的样子,由于每一组的 MVx 都是独立的,如何传递 Model?谁要负责控制传递的顺序?又该如何保留 Model 的状态?好吧!那就拆开...
拆开之后,问题似乎解决了,但此时高层又说了,这个程序要跨平台,所以二种类型的画面都要有...
Flux 所提供的效果
Flux 的架构则是打破这层胶着的状态,在其单向数据流的原则之下,View 只要管显示数据,不管数据的来源是一个还是多个。而被通知数据有异动时,也是依循相同的方式来获取数据,刷新画面。至于要如何异动数据与 View 无关,只要把异动的信息传出去,接着就像战机上的飞弹一样可以射后不理。
在这样的设计之下,以之前 Dashboard 的例子,不管是单一的画面负责显示所有的数据,还是画面上分割成许多不同的组件来分别显示特定的数据,都不会有设计上的违和感。而另一个例子同样也适用,无关后端的数据规划方式,View 只要专注在选墿合适的数据来源、考量如何显示数据上即可。
Flux 只能用在有 UI 的情境之下?不尽然,并不是只有人才会输入或需要取得回应。在有明确的边界之状况下,像是网络或是因设计的考量所形成逻辑上的 Layer,这种可以用来把数据供给端及接收端做有效的分离,以便进行分工、测试等等作业的架构,都可以考虑套用 Flux 的概念。
如何实现
俗话说得好,知易行难。了解 Flux 的运作过程是一回事,但要把这些过程落实到设计之中、形成源代码又是另外一回事。Facebook 并没有为 Java 的开发环境开发一套符合 Flux 的库,而 Java 的环境相较于 JavaScript 又更加地多元化,加大了使用上的不确定性。为了避免在开发上每次都要反覆进行类似的工作,于是就依据过去的工作经验,利用抽象化的手法及自动生成的概念,实现了一个 Framework,让想要在 Java 的项目中使用 Flux 的人可以轻易的上手。
接下来会针对这个 Framework 做个简单的说明。
取得 Binary
最新版本的 Jar 档可以在 Github 的 Release 页面中下载。
配置
如果是使用 Gradle 来建构程序,则所下载到的档案可以送到 build.gradle
配置引用的文件夹下。如果是 Android 的项目,则是放到 libs
的文件夹下即可。
在项目中有使用 fluxjava-rx
时,应该也会需要在 build.gradle
中增加以下的内容:
dependencies {
...
compile "io.reactivex:rxjava:1.2.+”
}
在使用之前
在 Github 的 Repository 中,FluxJava 库源代码放在 fluxjava
的文件夹下,并且在 demo-eventbus
文件夹下搭配一个示范用的 Android 项目。这个示范的项目是一个只有单一 Activity 的简易 Todo 应用程序。在这个 App 中可以展示以下的功能:
- 显示 Todo 清单
- 在不同使用者间切换 Todo 清单
- 新增 Todo
- 关闭/重启 Todo
在这个示范项目中使用 greenrobot 的 EventBus 来协助 Dispatcher 和 Store 发送信息。
如果想要与 RxJava 搭配使用,可以看一下 fluxjava-rx
文件夹,里面有 FluxJava 为 RxJava 所开发的 Addon。同时,有一个与之配对的示范项目在 demo-rx
文件夹下,是由 demo-eventbus
复制过来修改的。在这个示范项目中,原本的 EventBus 以 fluxjava-rx
所提供的 RxBus 取代。而基于 RxJava 1.x 库的 RxBus 所提供的功能和 EventBus 的功能相同。
如何使用
准备工作
Bus
Dispatcher 和 Store 会呼叫 Bus 来传送信息。Bus 必须要实现 IFluxBus 的介面,实现时可以使用任何的 Bus 方案,像是:Otto、EventBus,或是自行开发的方案。如果有同时引用fluxjava-rx
,则可以直接使用 RxBus 来提供传送信息的功能。Action
Dispatcher 使用 Action 来通知 Store 要进行的工作。在 Action 中有二个属性,一个是 Type、一个是 Data。Type 用来让 Store 识别要对数据进行的动作,Data 则是该动作的附属信息。以示范的项目来说,当一个新的 Todo 从介面上被传进来,则新 Todo 的内容会被放在 Data 栏位中。ActionHelper
ActionHelper 协助 ActionCreator 决定产生何种 Action,并且协助 ActionCreator 将目前传进来的数据格式转成可被处理的格式。Store
Store 负责截收由 Dispatcher 所送出的 Action,并根据 Action 上的信息进行对应的数据处理。当数据处理完成,Store 会再送出一个数据异动的事件,让事件的接收者可用以反应新的数据状态。StoreMap
StoreMap 是一个一对一的对照表,在 Framework 中使用这一个对照表来产生需要的 Store Instance。假设 Action 和 Store 的关系是一对一的,则 Action 的型别可以用来做为 Store 型别的键值,或是很单纯地使用一个数值来做为键值。像是在示范的项目中可以看到有二个常数在Constants.java
中,分别是 DATA_USER 及 DATA_TODO,这二个常数各自会对应到一个 Store 的型别。因此,与 TodoAction 配对的 TodoStore 就会被产生来负责处理与 Todo 相关的数据要求,而 User 也是套用一样的逻辑。
初始化程序
在 FluxJava 中,FluxContext 是用来做为整个程序开始的进入点。FluxContext 被设计成 Singleton,负责整合 Framework 中相关的组件,并且管理特定组件的 Instance。
FluxContext 的 Instance 可以由其内含的 Builder 来建立,示范的源代码如下:
FluxContext.getBuilder()
.setBus(new Bus())
.setActionHelper(new ActionHelper())
.setStoreMap(storeMap)
.build();
开始发送要求
在取得使用者透过 UI 组件所输入的数据后,接下来可以利用 ActionCreator 来推送 Action,ActionCreator 的 Instance 可经由 FluxContext 来取得。Framework 预设所提供的 ActionCreator 只有一项功能 sendRequest
,呼叫的源代码要传入 Id 及使用者输入的数据。其中,Id 是用来决定要产生的 Action 型别。使用者输入的数据可以在呼叫 sendRequest
后,经由 ActionHelper 转成 Store 所需的格式。
以下为示范的源代码:
Todo todo = new Todo();
FluxContext.getInstance()
.getActionCreator()
.sendRequestAsync(TODO_ADD, todo);
sendRequest
有提供二种版本的实现,同步和非同步。非同步的版本会先建立一个新的 Thread 之后,在新的 Thread 中执行。如果需要特别管控 Thread 的使用或是想要使用 Thread Pool,则可以呼叫同步的版本来达到目的。
进行数据处理
要进行数据处理需在 Store 中拦截指定的 Action,拦截的方法会依据所使用的 Bus 方案而不同。以示范项目的例子来说,要在 Store 中新增一个搭配特定 Annotation 的方法。相关的程序范例如下:
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onAction(final TodoAction inAction) {
switch (inAction.getType()) {
case TODO_LOAD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_ADD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_CLOSE:
...
super.emitChange(new ItemChangeEvent(i));
break;
}
}
如果是使用 fluxjava-rx
,则 Store 可以继承自 RxStore,此时只要改写 RxStore 中的 onAction
方法即可。相关的程序范例如下:
@Override
protected <TAction extends IFluxAction> void onAction(final TAction inAction) {
final TodoAction action = (TodoAction)inAction;
switch (action.getType()) {
case TODO_LOAD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_ADD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_CLOSE:
...
super.emitChange(new ItemChangeEvent(i));
break;
}
}
反应数据异动
跟 Store 一样,UI 组件要依据使用的 Bus 方案来接收由 Store 所发出的数据异动事件。在 EventBus 的例子中:
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(final TodoStore.ListChangeEvent inEvent) {
super.notifyDataSetChanged();
}
在 RxBus 的例子中:
todoStore.toObservable(TodoStore.ListChangeEvent.class)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Action1<TodoStore.ListChangeEvent>() {
@Override
public void call(final TodoStore.ListChangeEvent inEvent) {
TodoAdapter.super.notifyDataSetChanged();
}
});