上篇講述了增強(qiáng)邏輯功能測試而改進(jìn)MVC為MVP,但是這樣做可能還不夠徹底,現(xiàn)在來討論另一個純粹從測試角度設(shè)計的框架。
首先我們來明確一下,測試中最核心的東西是什么。當(dāng)然是數(shù)據(jù),我們永遠(yuǎn)是圍繞著數(shù)據(jù)來的,那么之前一些架構(gòu)的問題是什么。無論哪個框架,數(shù)據(jù)的流通都是雙向的,當(dāng)數(shù)據(jù)流通成為單向了會怎么樣呢?
in data ==> Module ==> out data
這樣我們偽造數(shù)據(jù)進(jìn)行測試就會非常方便了。按照這個思想就有了數(shù)據(jù)單向流通的架構(gòu)。
數(shù)據(jù)單向流通的實(shí)現(xiàn)
這個概念最早是在web中提出的,應(yīng)用在React里,官方的方案是Redux
。現(xiàn)在swift也提出了一種實(shí)現(xiàn)ReSwift
。
我在之前寫React的時候使用過這種方案,從開發(fā)角度來說,這種方案會大大增加開發(fā)難度,代碼量也會大量增加,而且開發(fā)思路也需要從以前的思考方式轉(zhuǎn)換過來。但是如果我們把這個思路轉(zhuǎn)換過來,其實(shí)對整個流程是更加簡化和分離的。
從測試角度看,我覺得無疑是我知道的最可測的一種框架,甚至可以測試部分視圖的邏輯。
那么總的來說,很難說這種結(jié)構(gòu)的好壞,就算不考慮增加的開發(fā)時間,也是一種難以給以一種評價的方案。
(Redux/ReSwift)框架介紹
方案的幾個核心是:
- 數(shù)據(jù)的單向流通
- 每個視圖都可以看做一個狀態(tài)機(jī)
- pure function
關(guān)于pure function,我就不做太多介紹了,簡單的說,就是同一輸入必定會有相同的輸出,是非常容易測試的一種函數(shù)。
首先,我們來看一下官方的架構(gòu)圖。

可以看到,數(shù)據(jù)流動方向都是朝一個方向進(jìn)行的。那么下面從每個模塊來介紹下,還是以star button為例子。
State
視圖狀態(tài)機(jī),也是所有會更新界面數(shù)據(jù)保存的地方,可以認(rèn)為相當(dāng)于ViewModel。
首先我們star會有以下幾種視覺樣式
enum StarButtonState {
case star
case staring
case unstar
case unstaring
}
所以State可以定義為
struct StarState: StateType {
var state: StarButtonState
var starCount
}
Action
首先我們定義幾種狀態(tài)機(jī)轉(zhuǎn)換的Action類型
struct StarAction: Action { }
struct StaringAction: Action { }
struct UnstarAction: Action { }
struct UnstaringAction: Action { }
以及相應(yīng)的功能以及狀態(tài)變更,這里異步請求采用延遲來代表。
func star(id: String) -> Store<StarState>.ActionCreator {
return { state, store in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
store.dispatch(StarAction())
}
return StaringAction()
}
}
func unstar(id: String) -> Store<StarState>.ActionCreator {
return { state, store in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
store.dispatch(UnstarAction())
}
return UnstaringAction()
}
}
View
視圖層其實(shí)很簡單,只需要根據(jù)State的不同來更新就可以了。注意的是,更新都是無狀態(tài)的,和上一個狀態(tài)無關(guān),所以view層是個無狀態(tài)層。
class StarButton: UIButton, StoreSubscriber {
let store = Store<StarState>(reducer: starReducer, state: nil)
override init(frame: CGRect) {
super.init(frame: frame)
self.store.subscribe(self)
}
func newState(state: StarState) {
// update UI
}
}
Reducer
狀態(tài)轉(zhuǎn)換器,唯一可以更新State的地方。
func starReducer(action: Action, state: StarState?) -> StarState {
var state = state ?? StarState(state: .star, starCount: 0)
switch action {
case _ as StarAction:
state.state = .star
state.starCount += 1
case _ as StaringAction:
state.state = .staring
case _ as UnstarAction:
state.state = .unstar
state.starCount -= 1
case _ as UnstaringAction:
state.state = .unstaring
default:
break
}
return state
}
數(shù)據(jù)傳遞
那么最重要的就是數(shù)據(jù)如何傳遞的了。首先要明確的是每個模塊能夠修改的,或者說是傳遞的,只能是下個模塊。
比如,用戶star button觸發(fā)了一個事件:
func onButton(sender: StarButton) {
if (store.state.state == .unstar) {
store.dispatch(star(id: id))
}
else if (store.state.state == .star) {
store.dispatch(unstar(id: id))
}
}
此時會創(chuàng)建Action,也就是將view事件轉(zhuǎn)換為Action。然后會傳遞到store中,store會調(diào)用Reducer進(jìn)行處理。Reducer更新state之后又會觸發(fā)store的subscribe事件,回到view的func newState(state: StarState)
。
View (User Event)
==(create)==> ActionCreator/Action
==(dispatch)==> Store <--(Update State)--> Reducer
\==(subscribe)==> View (newState)
大概的一個流程就是這樣了。
接下來說說這樣做的模塊化的優(yōu)勢。
模塊化和測試性
首先,我們需要有函數(shù)式編程的概念,函數(shù)也是一等公民,所以ActionCreator
和Reducer
都是獨(dú)立的模塊。
作為使用者,我們在不需要像MVC一樣知道這些api所代表的操作功能,相對應(yīng)的,我們需要去了解一個模塊的動作(Action),比如以上例子就是
func star(id: String)
func unstar(id: String)
這樣的劃分比MVC要友好的多,真正的把邏輯功能從原本的C中分離開。需要觸發(fā)這個行為也非常簡單store.dispatch(star(id: id))
。相比MVP,行為更加的獨(dú)立,每個行為之間完全沒有聯(lián)系,也不會產(chǎn)生干擾影響。同時因?yàn)槊總€行為的獨(dú)立性,可復(fù)用程度也就越高。
Reducer則代表了view層的更新,也可以非常明確的知道每個狀態(tài)的變更發(fā)生了什么。相比其他模式,將界面更新完全交給view或者Controller,Reducer是最明確也是最清晰的。同時Reducer也是獨(dú)立的,可以替換的。
對于UIkit層面我們無法單元測試,所以測試的主要部分是Action
和Reducer
。這兩個模塊可以說都是pure function
或者在某些條件下是pure function
的,所以測試也非常的簡單。
對比
和這個模式比較像的有狀態(tài)機(jī)模式和Reactive。
狀態(tài)機(jī)模式也是實(shí)現(xiàn)對應(yīng)功能,以及對應(yīng)狀態(tài),然后通過子類化的方式去實(shí)現(xiàn)Reducer的功能。
Reactive則比較像ActionCreator,只是Reactive返回的是信號量。
使用場景
從上面可以看出這是一套非常優(yōu)秀的模塊劃分方案,但同時也會大大增加代碼量,而且需要改變以前的思維模式。而對于目前國內(nèi)的現(xiàn)狀來看,很難有這么多時間和精力讓整個項目都使用這種模式。
但是這種模式的特點(diǎn)也非常的明顯,在處理比較復(fù)雜的交互行為,并且存在較多的視圖狀態(tài)的時候,會是一種比較好的方案。比如視頻播放界面。
所以個人認(rèn)為,在一些簡單的場景下并不需要使用該方案,但是在一些復(fù)雜的交互頁面,而且又非常想要引入單元測試的場景,可以酌情考慮下這種方案。這種方案要求人們的思維方式的改變,需要有一定的函數(shù)式編程的概念。
雖然不一定會直接使用ReSwift,但是這種思想有很多值得借鑒的地方,利用這種思想做出類似的效果,以便達(dá)到可以容易進(jìn)行白盒測試的目的。
最后
以上雖然說不會全部使用該方案,但也可以部分使用。比如獨(dú)立的小模塊,亦或是app層面的一些東西。下次可以討論下app層面如何來利用單向數(shù)據(jù)流來簡化流程。