上篇講述了增強(qiáng)邏輯功能測(cè)試而改進(jìn)MVC為MVP,但是這樣做可能還不夠徹底,現(xiàn)在來(lái)討論另一個(gè)純粹從測(cè)試角度設(shè)計(jì)的框架。
首先我們來(lái)明確一下炼幔,測(cè)試中最核心的東西是什么。當(dāng)然是數(shù)據(jù)史简,我們永遠(yuǎn)是圍繞著數(shù)據(jù)來(lái)的乃秀,那么之前一些架構(gòu)的問(wèn)題是什么。無(wú)論哪個(gè)框架圆兵,數(shù)據(jù)的流通都是雙向的跺讯,當(dāng)數(shù)據(jù)流通成為單向了會(huì)怎么樣呢?
in data ==> Module ==> out data
這樣我們偽造數(shù)據(jù)進(jìn)行測(cè)試就會(huì)非常方便了殉农。按照這個(gè)思想就有了數(shù)據(jù)單向流通的架構(gòu)刀脏。
數(shù)據(jù)單向流通的實(shí)現(xiàn)
這個(gè)概念最早是在web中提出的,應(yīng)用在React里超凳,官方的方案是Redux
∮郏現(xiàn)在swift也提出了一種實(shí)現(xiàn)ReSwift
。
我在之前寫React的時(shí)候使用過(guò)這種方案轮傍,從開(kāi)發(fā)角度來(lái)說(shuō)暂雹,這種方案會(huì)大大增加開(kāi)發(fā)難度,代碼量也會(huì)大量增加创夜,而且開(kāi)發(fā)思路也需要從以前的思考方式轉(zhuǎn)換過(guò)來(lái)杭跪。但是如果我們把這個(gè)思路轉(zhuǎn)換過(guò)來(lái),其實(shí)對(duì)整個(gè)流程是更加簡(jiǎn)化和分離的驰吓。
從測(cè)試角度看涧尿,我覺(jué)得無(wú)疑是我知道的最可測(cè)的一種框架,甚至可以測(cè)試部分視圖的邏輯檬贰。
那么總的來(lái)說(shuō)姑廉,很難說(shuō)這種結(jié)構(gòu)的好壞,就算不考慮增加的開(kāi)發(fā)時(shí)間偎蘸,也是一種難以給以一種評(píng)價(jià)的方案庄蹋。
(Redux/ReSwift)框架介紹
方案的幾個(gè)核心是:
- 數(shù)據(jù)的單向流通
- 每個(gè)視圖都可以看做一個(gè)狀態(tài)機(jī)
- pure function
關(guān)于pure function,我就不做太多介紹了迷雪,簡(jiǎn)單的說(shuō)限书,就是同一輸入必定會(huì)有相同的輸出,是非常容易測(cè)試的一種函數(shù)章咧。
首先倦西,我們來(lái)看一下官方的架構(gòu)圖。
可以看到赁严,數(shù)據(jù)流動(dòng)方向都是朝一個(gè)方向進(jìn)行的扰柠。那么下面從每個(gè)模塊來(lái)介紹下,還是以star button為例子疼约。
State
視圖狀態(tài)機(jī)卤档,也是所有會(huì)更新界面數(shù)據(jù)保存的地方,可以認(rèn)為相當(dāng)于ViewModel程剥。
首先我們star會(huì)有以下幾種視覺(jué)樣式
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)變更劝枣,這里異步請(qǐng)求采用延遲來(lá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í)很簡(jiǎn)單织鲸,只需要根據(jù)State的不同來(lái)更新就可以了舔腾。注意的是,更新都是無(wú)狀態(tài)的搂擦,和上一個(gè)狀態(tài)無(wú)關(guān)稳诚,所以view層是個(gè)無(wú)狀態(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ù)如何傳遞的了。首先要明確的是每個(gè)模塊能夠修改的橱夭,或者說(shuō)是傳遞的普办,只能是下個(gè)模塊。
比如徘钥,用戶star button觸發(fā)了一個(gè)事件:
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))
}
}
此時(shí)會(huì)創(chuàng)建Action衔蹲,也就是將view事件轉(zhuǎn)換為Action。然后會(huì)傳遞到store中呈础,store會(huì)調(diào)用Reducer進(jìn)行處理舆驶。Reducer更新state之后又會(huì)觸發(fā)store的subscribe事件,回到view的func newState(state: StarState)
而钞。
View (User Event)
==(create)==> ActionCreator/Action
==(dispatch)==> Store <--(Update State)--> Reducer
\==(subscribe)==> View (newState)
大概的一個(gè)流程就是這樣了沙廉。
接下來(lái)說(shuō)說(shuō)這樣做的模塊化的優(yōu)勢(shì)。
模塊化和測(cè)試性
首先臼节,我們需要有函數(shù)式編程的概念撬陵,函數(shù)也是一等公民珊皿,所以ActionCreator
和Reducer
都是獨(dú)立的模塊。
作為使用者巨税,我們?cè)诓恍枰馦VC一樣知道這些api所代表的操作功能蟋定,相對(duì)應(yīng)的,我們需要去了解一個(gè)模塊的動(dòng)作(Action)草添,比如以上例子就是
func star(id: String)
func unstar(id: String)
這樣的劃分比MVC要友好的多驶兜,真正的把邏輯功能從原本的C中分離開(kāi)。需要觸發(fā)這個(gè)行為也非常簡(jiǎn)單store.dispatch(star(id: id))
远寸。相比MVP抄淑,行為更加的獨(dú)立,每個(gè)行為之間完全沒(méi)有聯(lián)系驰后,也不會(huì)產(chǎn)生干擾影響肆资。同時(shí)因?yàn)槊總€(gè)行為的獨(dú)立性,可復(fù)用程度也就越高灶芝。
Reducer則代表了view層的更新迅耘,也可以非常明確的知道每個(gè)狀態(tài)的變更發(fā)生了什么。相比其他模式监署,將界面更新完全交給view或者Controller颤专,Reducer是最明確也是最清晰的。同時(shí)Reducer也是獨(dú)立的钠乏,可以替換的栖秕。
對(duì)于UIkit層面我們無(wú)法單元測(cè)試,所以測(cè)試的主要部分是Action
和Reducer
晓避。這兩個(gè)模塊可以說(shuō)都是pure function
或者在某些條件下是pure function
的簇捍,所以測(cè)試也非常的簡(jiǎn)單。
對(duì)比
和這個(gè)模式比較像的有狀態(tài)機(jī)模式和Reactive俏拱。
狀態(tài)機(jī)模式也是實(shí)現(xiàn)對(duì)應(yīng)功能暑塑,以及對(duì)應(yīng)狀態(tài),然后通過(guò)子類化的方式去實(shí)現(xiàn)Reducer的功能锅必。
Reactive則比較像ActionCreator事格,只是Reactive返回的是信號(hào)量。
使用場(chǎng)景
從上面可以看出這是一套非常優(yōu)秀的模塊劃分方案搞隐,但同時(shí)也會(huì)大大增加代碼量驹愚,而且需要改變以前的思維模式。而對(duì)于目前國(guó)內(nèi)的現(xiàn)狀來(lái)看劣纲,很難有這么多時(shí)間和精力讓整個(gè)項(xiàng)目都使用這種模式逢捺。
但是這種模式的特點(diǎn)也非常的明顯,在處理比較復(fù)雜的交互行為癞季,并且存在較多的視圖狀態(tài)的時(shí)候劫瞳,會(huì)是一種比較好的方案倘潜。比如視頻播放界面。
所以個(gè)人認(rèn)為志于,在一些簡(jiǎn)單的場(chǎng)景下并不需要使用該方案涮因,但是在一些復(fù)雜的交互頁(yè)面,而且又非常想要引入單元測(cè)試的場(chǎng)景恨憎,可以酌情考慮下這種方案。這種方案要求人們的思維方式的改變郊楣,需要有一定的函數(shù)式編程的概念憔恳。
雖然不一定會(huì)直接使用ReSwift,但是這種思想有很多值得借鑒的地方净蚤,利用這種思想做出類似的效果钥组,以便達(dá)到可以容易進(jìn)行白盒測(cè)試的目的。
最后
以上雖然說(shuō)不會(huì)全部使用該方案今瀑,但也可以部分使用程梦。比如獨(dú)立的小模塊,亦或是app層面的一些東西橘荠。下次可以討論下app層面如何來(lái)利用單向數(shù)據(jù)流來(lái)簡(jiǎn)化流程屿附。