測(cè)試驅(qū)動(dòng)開(kāi)發(fā)
1.什么是測(cè)試驅(qū)動(dòng)開(kāi)發(fā)祈纯?
基本思想就是在開(kāi)發(fā)功能代碼之前您觉,先編寫(xiě)測(cè)試代碼,然后只編寫(xiě)使測(cè)試通過(guò)的功能代碼庐橙,從而以測(cè)試來(lái)驅(qū)動(dòng)整個(gè)開(kāi)發(fā)過(guò)程的進(jìn)行。這有助于編寫(xiě)簡(jiǎn)潔可用和高質(zhì)量的代碼借嗽,有很高的靈活性和健壯性态鳖,能快速響應(yīng)變化,并加速開(kāi)發(fā)過(guò)程恶导。
我并不是說(shuō)以后寫(xiě)業(yè)務(wù)浆竭,都要先寫(xiě)單元測(cè)試,然后寫(xiě)完單元測(cè)試后開(kāi)始寫(xiě)業(yè)務(wù)惨寿。這會(huì)拉長(zhǎng)開(kāi)發(fā)周期邦泄,再說(shuō)老板也不答應(yīng)呀。但我一直認(rèn)為高度可測(cè)試的代碼的可維護(hù)性更強(qiáng)裂垦。所以可以在平時(shí)寫(xiě)代碼前顺囊,多想想為自己的代碼添加單元測(cè)試麻煩嗎,是不是還有可測(cè)試性更高的寫(xiě)法缸废。
但往往我們?cè)诮o自己的App添加單元測(cè)式包蓝,如果在寫(xiě)代碼的時(shí)候沒(méi)有過(guò)多的思考,寫(xiě)了很多膠水代碼企量。編寫(xiě)寫(xiě)單元測(cè)試會(huì)變得很困難测萎。無(wú)論是UI測(cè)試還是功能測(cè)試。如果改UI的代碼散落各處届巩,一個(gè)方法受狀態(tài)的影響太多硅瞧。
而且狀態(tài)可能來(lái)自服務(wù)器(http請(qǐng)求的回調(diào)),用戶(hù)操作(Target - Action)恕汇,一個(gè)方法依賴(lài)的外部狀態(tài)太多了腕唧,要寫(xiě)單元測(cè)試得用一些測(cè)試庫(kù)如ocmork,來(lái)模擬一些具體的依賴(lài)類(lèi)瘾英,模擬相應(yīng)或者返回枣接,而且還的配合用戶(hù)的點(diǎn)擊狀態(tài)。那么寫(xiě)一個(gè)函數(shù)的測(cè)試變的很復(fù)雜缺谴。而且一個(gè)VC如果很多都由這種代碼組成但惶,重構(gòu)困難,加功能困難,需要改時(shí)候容易牽一發(fā)動(dòng)全身膀曾,這個(gè)類(lèi)也變得難以維護(hù)县爬。
2.設(shè)計(jì)階段多思考可測(cè)試性
如果一個(gè)模塊(大模塊或者一個(gè)MVC模塊)變得容易測(cè)試,覆蓋范圍廣添谊,可維護(hù)性一定強(qiáng)财喳。對(duì)于一個(gè)函數(shù)或者方法,怎么樣才比較容易測(cè)試斩狱,測(cè)試無(wú)非就是模擬輸入局扶,驗(yàn)證輸出怠苔。如果一個(gè)方法和輸入和輸出明確少漆,類(lèi)似于y= f(x),那么我們只要模擬x题禀,驗(yàn)證y卡睦。就可以完成這個(gè)函數(shù)的單元測(cè)試悍手。而這個(gè)f(x)的內(nèi)部實(shí)現(xiàn)不會(huì)依賴(lài)于函數(shù)外部的參數(shù)证薇,也就是說(shuō)這個(gè)f(x)是獨(dú)立的桥氏,沒(méi)有副作用的函數(shù)乍赫。這個(gè)理想的函數(shù)有純函數(shù)的一些特性瓣蛀。那么什么是純函數(shù)?
3.純函數(shù)
指一個(gè)函數(shù)如果有相同的輸入雷厂,則它產(chǎn)生相同的輸出惋增,一旦輸入給定,那么輸出則唯一確定改鲫,沒(méi)有負(fù)作用诈皿。比如y = sin(X)。
像我們平時(shí)寫(xiě)的[someInstance method];這樣的函數(shù)由于不存在輸入和輸出像棘,如果內(nèi)部實(shí)現(xiàn)還對(duì)外部的結(jié)構(gòu)參數(shù)做一些改變稽亏,就很難直接測(cè)試這個(gè)函數(shù)。函數(shù)內(nèi)部如果對(duì)參數(shù)意外的變量或者狀態(tài)進(jìn)行改變缕题,這個(gè)就是這個(gè)函數(shù)的副作用(side effect)截歉,也可以說(shuō)這個(gè)函數(shù)對(duì)其他變量的依賴(lài)性強(qiáng),而且單看這個(gè)函數(shù)本身烟零,還預(yù)見(jiàn)不了這個(gè)改變瘪松,依賴(lài)被函數(shù)所屏蔽了。這是一個(gè)容易引起bug的潛在因素锨阿。
4.純函數(shù)特點(diǎn)
先偏離App宵睦,純函數(shù)編程有以下特點(diǎn):
4.1.函數(shù)可以被當(dāng)成參數(shù)和output。
4.2.函數(shù)的結(jié)果只受函數(shù)參數(shù)影響墅诡,不依賴(lài)于定義在函數(shù)外的變量壳嚎,對(duì)于特定輸入有特定輸出
4.3.依賴(lài)于不可變數(shù)據(jù)結(jié)構(gòu)
4.4.由于計(jì)算是透明的,任何時(shí)候執(zhí)行產(chǎn)生相同結(jié)果,可以推遲計(jì)算诬辈,知道需要的時(shí)候酵使。lazy load 或者 swift的copy on write
4.5利用范型進(jìn)行高度抽象成功能性程序。
4.6.函數(shù)為一等公民焙糟,可以作為參數(shù)和返回值口渔。而且還可以延遲執(zhí)行。
其實(shí)Swift是一個(gè)很好實(shí)踐函數(shù)式編程的一門(mén)得力語(yǔ)言穿撮,但我們今天不講swift的函數(shù)式編程缺脉,如果對(duì)Swift的函數(shù)式編程思想感興趣,可以參考objc.io上的advanced-swift 和functional-swift悦穿,我這里有中文的譯本攻礼。
5.實(shí)踐
5.1 如果你們和我一樣現(xiàn)在還在用OC,其實(shí)作為一個(gè)純面向?qū)ο蟮木幊陶Z(yǔ)言栗柒,但編程的時(shí)候以函數(shù)式思想編程還是有借鑒意義的礁扮。可以在這里找到具體例子demo瞬沦。在介紹具體的代碼前太伊,我想先介紹以下Redux:
Redux由Dan Abramov在2015年創(chuàng)建。是受2014年Facebook的Flux架構(gòu)以及函數(shù)式編程語(yǔ)言Elm啟發(fā)逛钻。Redux因其簡(jiǎn)單易學(xué)體積小在短時(shí)間內(nèi)成為最熱門(mén)的前端架構(gòu)僚焦。
它有以下幾個(gè)核心概念:
Action:簡(jiǎn)單地,Actions就是事件曙痘。用戶(hù)的操作芳悲,網(wǎng)絡(luò)的回調(diào),Actions傳遞來(lái)自(用戶(hù)接口边坤,內(nèi)部事件比如API調(diào)用和表單提交)的數(shù)據(jù)給store名扛。store只獲取來(lái)自Actions的信息。
Store:Store對(duì)象保存應(yīng)用的狀態(tài)并提供一些幫助方法來(lái)存取狀態(tài)茧痒,分發(fā)狀態(tài)以及注冊(cè)監(jiān)聽(tīng)罢洲。全部state由一個(gè)store來(lái)表示。任何action通過(guò)reducer返回一個(gè)新的狀態(tài)對(duì)象文黎。這就使得Redux非常簡(jiǎn)單以及可預(yù)測(cè)惹苗。
Reducer:在Redux中,reducer就是獲得這個(gè)應(yīng)用的當(dāng)前狀態(tài)和事件然后返回一個(gè)新?tīng)顟B(tài)的函數(shù)耸峭。(Action,State) ---> State
State:應(yīng)用的狀態(tài)桩蓉,決定著應(yīng)用的行為和輸出。
對(duì)于 app 而言劳闹,我們總是會(huì)和一定的用戶(hù)輸入打交道院究,也必然會(huì)需要按照用戶(hù)的輸入和已知狀態(tài)來(lái)更新 UI 作為“輸出”洽瞬。這個(gè)狀態(tài)我們可以抽象成State,用戶(hù)的輸入或者其他能夠改變狀態(tài)的行為我們抽象為Action业汰,那我們需要寫(xiě)自己的Reduce函數(shù)伙窃。設(shè)計(jì)Reduce函數(shù)的時(shí)候最好輸入給一個(gè)state,輸出一個(gè)全新的newState样漆,它們是不同的對(duì)象为障,而不僅僅只是在同一個(gè)對(duì)象的基礎(chǔ)上進(jìn)行改變,這樣在訂閱方才可以明確知道state是否發(fā)生改變放祟。
reducer(Action,State) -> State
我們還需要一個(gè)State來(lái)驅(qū)動(dòng)變化鳍怨,所以我認(rèn)為State結(jié)構(gòu)內(nèi)部可以引用一些驅(qū)動(dòng)用戶(hù)行為或者UI變化的數(shù)據(jù)源,最好確保 State 中每個(gè)節(jié)點(diǎn)都是 Immutable 的跪妥,這樣將確保 State 的消費(fèi)者在判斷數(shù)據(jù)是否變化時(shí)鞋喇,只要簡(jiǎn)單地進(jìn)行引用比較即可。
我們需要Store來(lái)存儲(chǔ)State眉撵,訂閱觀察者侦香,給State派發(fā)Action,所以一個(gè)Store可能抽象成這樣
@interface Store : NSObject
//當(dāng)前狀態(tài)
@property (nonatomic, strong,readonly) id<StateType> state;
//初始化方法纽疟,接受一個(gè)reducer和初始狀態(tài)
- (instancetype)initWithReducer:(Reducer )reducer
initialState:(id<StateType>)state;
//訂閱一個(gè)觀察者鄙皇,State發(fā)生改變通知觀察者
- (void)subscribeNext:(SubscribeBlock)subscriber;
//取消訂閱
- (void)unsubscribe;
//給State 派發(fā)Action
- (void)dispatch:(id<ActionType>)action;
@end
對(duì)于Action,生產(chǎn)者給Store dispatch Action仰挣。我們將Action分為同步的或者異步的,同步的Action通過(guò)Reduce產(chǎn)生新的State驅(qū)動(dòng)subscriber缠沈,異步的Action通過(guò)Reduce膘壶,這時(shí)候并不產(chǎn)生新的state,而是在回調(diào)中再向Store dispatch newAction 洲愤,再產(chǎn)生新的State后才驅(qū)動(dòng)subsriber颓芭。
數(shù)據(jù)的流動(dòng)就變成這樣:
解釋?zhuān)篠tore會(huì)持有State和reducer,外界如果想要觸發(fā)新的State只有通過(guò)向Store派發(fā)Action柬赐,Store拿到Action和當(dāng)前的State亡问,會(huì)嘗試通過(guò)Reudcer產(chǎn)生一個(gè)新的State,如果這個(gè)Action是同步的肛宋,那么reduce可以立即產(chǎn)生新的有效的State州藕,然后通知訂閱者,訂閱者根據(jù)最新的State來(lái)決定UI的樣式酝陈。如果Action是異步的床玻,reduce不會(huì)立即產(chǎn)生一個(gè)newState,而是在異步操作的回調(diào)中給Store派發(fā)一個(gè)新的同步的Action沉帮。外界任何其他角色不直接改變UI锈死,UI是由唯一的State所決定贫堰。這樣要測(cè)試這部分的業(yè)務(wù),我們只要在給Store派發(fā)可預(yù)見(jiàn)的Action待牵,然后在Subscriber中檢測(cè)輸出其屏。這套邏輯本身沒(méi)有依賴(lài)其他任何的UI狀態(tài),所以單元測(cè)試變得簡(jiǎn)單缨该。
看了這么多抽象的邏輯我們看具體的demo
這是一個(gè)查詢(xún)省的一個(gè)demo偎行,跳轉(zhuǎn)后,會(huì)用coreData記錄下查詢(xún)記錄压彭,搜索部分輸入省名還可以進(jìn)行查詢(xún)睦优。
我們看下Store類(lèi)的結(jié)構(gòu),Store初始化時(shí)候需要intialState和Reducer函數(shù),reducer要從外面?zhèn)魅氲脑蚴亲巢唬瑀educer要操作具體的State汗盘,這個(gè)State必定和業(yè)務(wù)綁定。為了解耦询一。值得一提的就是這個(gè)dispatch方法隐孽,store將訂閱者放到一個(gè)array容器里,接受到異步action的時(shí)候reducer會(huì)返回nil健蕊,我們就不通知訂閱者菱阵,否則執(zhí)行array的blocks
@interface Store : NSObject
@property (nonatomic, strong,readonly) id<StateType> state;
- (instancetype)initWithReducer:(Reducer )reducer
initialState:(id<StateType>)state;
- (void)subscribeNext:(SubscribeBlock)subscriber;
- (void)unsubscribe;
- (void)dispatch:(id<ActionType>)action;
@end
@implementation Store
...
- (void)dispatch:(id<ActionType>)action {
id<StateType> previousState = _state;
id<StateType> nextState = self.reducer(previousState,action);
if (nextState) {
self.state = nextState;
if (self.subscribers.count > 0) {
__weak __typeof(self)weakSelf = self;
[self.subscribers enumerateObjectsUsingBlock:
^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
@synchronized (weakSelf) {
SubscribeBlock block = (SubscribeBlock)obj;
block(previousState,nextState);
}
}];
}
}
}
@end
...
在ViewController里面我們定義了State和Action的數(shù)據(jù)結(jié)構(gòu),當(dāng)然也可以將這兩部分抽出來(lái)放在另一個(gè)service類(lèi)中缩功,但我這里就這么做了晴及。
@interface State :NSObject<StateType,NSCopying>
@property (nonatomic, copy) NSArray *cities;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, copy) NSArray *histories;
@end
@implementation State
- (id)copyWithZone:(NSZone *)zone {
State *copy = [[[self class] allocWithZone:zone] init];
copy.cities = self.cities;
copy.text = self.text;
copy.histories = self.histories;
return copy;
}
@end
我認(rèn)為State就管理著數(shù)據(jù)源,因?yàn)镾tate決定著程序的行為和UI的樣式嫡锌,而一般這些都是一些特定的數(shù)據(jù)所驅(qū)動(dòng)的虑稼。這里整個(gè)demo的數(shù)據(jù)源有,所有省份(citites)势木,搜索的文字(text)和歷史記錄(history)三部分組成蛛倦,注意這里用的都是不可變得數(shù)據(jù)結(jié)構(gòu),state遵循了NSCopying協(xié)議啦桌,因?yàn)榻?jīng)過(guò)reducer的后的state和之前的state需要是兩個(gè)不同的State溯壶。
對(duì)于Action,區(qū)分了異步和同步的Action甫男,它們?cè)赗educer中的處理不一樣且改。
typedef NS_ENUM(NSUInteger, Action_Type) {
UpdateText_Action,
AddCities_Action,
AddHistories_Action,
//異步command
FetchCities_Action,
FetchHistories_Action,
FetchAssociate_Action,
ClearHistory_Action,
};
@interface Action :NSObject<ActionType>
@property (nonatomic, assign) Action_Type actionType;
@property (nonatomic, strong) id associateValues;
+ (instancetype)actionWithActionType:(Action_Type) type values:(id)associateValues;
@end
我們看這個(gè)重要的Reducer是如何被定義的,對(duì)于收到同步的AddHistories_Action
板驳,我們?cè)O(shè)置屬性后返回新的State钾虐,對(duì)于異步的FetchHistories_Action
,我們?cè)诨卣{(diào)中再發(fā)起一個(gè)新的同步的Action笋庄。
- (Reducer )reducer {
__weak __typeof(self)weakSelf = self;
Reducer reducer = ^(id<StateType> state, id<ActionType>action){
State *previousState = (State *)state;
State *currentState = [previousState copy];
switch (action.actionType) {
.....省略一些代碼
case AddHistories_Action:
{
id associateValue = action.associateValues;
currentState.histories = associateValue;
break;
}
case FetchHistories_Action: {
[FetchData fetchHistories:^(NSArray *data, NSError *error) {
Action *action = [Action new];
action.actionType = AddHistories_Action;
action.associateValues = data;
[weakSelf.store dispatch:action];//2
}];
currentState = nil;
break;
}
default:
break;
}
return currentState;
};
return reducer;
}
我們?cè)趘iewDidload中我們1.初始化State效扫,2.初始化Store倔监,3.綁定訂閱者,4.發(fā)起查詢(xún)省的一個(gè)Action
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"clearHistory"
style:UIBarButtonItemStylePlain
target:self
action:@selector(clearHistory)];
[self.textField addTarget:self
action:@selector(changed:)
forControlEvents:UIControlEventEditingChanged];
//1.初始化State
State *initialState = [[State alloc] init];
//2.初始化Store
_store = [[Store alloc] initWithReducer:self.reducer initialState:initialState];
__weak __typeof(self)weakSelf = self;
[_store subscribeNext:^(State *old ,State *new) {
//3.綁定訂閱者
[weakSelf stateDidChangeWithNew:new old:old];
}];
//4.發(fā)起查詢(xún)省的一個(gè)Action
Action *fetchCitiesAction = [Action actionWithActionType:FetchCities_Action values:nil];
[_store dispatch:fetchCitiesAction];//3
}
那么在訂閱者受到通知后菌仁,我們就可以根據(jù)newState和oldState來(lái)決定UI樣式了,這樣我們就把對(duì)UI的管理集中在了一處浩习,外界只有通過(guò)向Store發(fā)送Action的形式才能改變State,而State又是唯一決定UI的元素济丘。那么代碼的邏輯是不是看起來(lái)就比較清晰了谱秽。
- (void)stateDidChangeWithNew:(State *)new old:(State *)old{
NSLog(@"old = %@,new = %@",old.description,new.description);
if (old.cities == nil || new.cities != old.cities) {
//這里比較指針就好,因?yàn)榻?jīng)過(guò)reduce的是兩個(gè)不同的state摹迷,而且屬性都是不可變的疟赊。
NSIndexSet *set = [[NSIndexSet alloc] initWithIndex:CitiesSection];
[self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationFade];
}
if (old.histories == nil || new.histories != old.histories) {
//這里比較指針就好,因?yàn)榻?jīng)過(guò)reduce的是兩個(gè)不同的state峡碉,而且屬性都是不可變的近哟。
NSIndexSet *set = [[NSIndexSet alloc] initWithIndex:HistorySection];
[self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationFade];
}
// [self.tableView reloadData];
//update title
if (new.text == nil) {
self.title = @"省";
} else {
self.title = new.text;
}
}
總結(jié)
其實(shí)這一套理論和語(yǔ)言本身無(wú)關(guān),和UI也沒(méi)有關(guān)系鲫寄,更像是一種設(shè)計(jì)思想吉执,我們可以把它用在任何無(wú)關(guān)UI的地方,有點(diǎn)向游戲設(shè)計(jì)中的狀態(tài)機(jī)的設(shè)計(jì)思路地来。但無(wú)論怎樣戳玫,這樣的代碼確實(shí)比習(xí)慣的膠水代碼可測(cè)試性更強(qiáng)。如果在Demo中我們需要添加一個(gè)新的邏輯未斑,我們只要在Action中添加一個(gè)新類(lèi)型咕宿,State里面加一個(gè)新的數(shù)據(jù)源,在reduce里面處理蜡秽,然后在stateDidChangeWithNew:old:
處理府阀,代碼的邏輯依然清晰可見(jiàn)。如果喜歡請(qǐng)點(diǎn)個(gè)贊载城,或者star,Have Fun费就!??????