為何選擇 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 中增加以下的內容:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 型別的鍵值。像是在示範的專案中可以看到有二個 Action,分別是 UserAction 及 TodoAction,並且各自會對應到一個 Store 的型別。因此,與 TodoAction 配對的 TodoStore 就會被產生來負責處理與 Todo 相關的資料要求。
初始化程序
在 FluxJava 中,FluxContext 是用來做為整個程序開始的進入點。FluxContext 被設計成 Singleton,負責整合 Framework 中相關的元件,並且管理特定元件的 Instance。FluxContext 的 Instance 可以由其內含的 Builder 來建立,示範的程式碼如下:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 所需的格式。以下為示範的程式碼:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Todo todo = new Todo(); | |
FluxContext.getInstance() | |
.getActionCreator() | |
.sendRequestAsync(TODO_ADD, todo); |
進行資料處理
要進行資料處理需在 Store 中攔截指定的 Action,攔截的方法會依據所使用的 Bus 方案而不同。以示範專案的例子來說,要在 Store 中新增一個搭配特定 Annotation 的方法。相關的程式範例如下:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 的例子中:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Subscribe(threadMode = ThreadMode.MAIN) | |
public void onEvent(final TodoStore.ListChangeEvent inEvent) { | |
super.notifyDataSetChanged(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
todoStore.toObservable(TodoStore.ListChangeEvent.class) | |
.observeOn(AndroidSchedulers.mainThread()) | |
.subscribe( | |
new Action1<TodoStore.ListChangeEvent>() { | |
@Override | |
public void call(final TodoStore.ListChangeEvent inEvent) { | |
TodoAdapter.super.notifyDataSetChanged(); | |
} | |
}); |
0 意見:
張貼留言