前言
鏈式調用chained calls
指在函數調用返回了一個對象的時候使得這個調用鏈可以不斷的調用下去,從概念上可以看做是一環扣一環的鐵鏈,也能被稱作方法鏈調用。
假設需求是在網絡請求完成之后先篩選過期數據,然后轉換成對應的數據模型進行展示。在Swift
中可以直接這么寫:
let dataArr = result["data"] as! [Dictionary]
self.models = dataArr.filter{ $0["status"] == "1" }.map{ Model($0) }
而OC
的語法決定了這步操作不能像Swift
一樣簡潔,最常見的代碼就是這樣:
NSArray * dataArr = result[@"data"];
NSMutableArray * models = @[].mutableCopy;
for (NSDictionary * dict in dataArr) {
if ([dict[@"status"] isEqualToString: @"1"]) {
[models addObject: [[Model alloc] initWithDict: dict]];
}
}
self.models = [NSArray arrayWithArray: models];
對比兩段代碼,不難看出方法鏈調用的優點包括:
- 代碼簡潔優雅,可讀性強
- 減少了重復使用同一變量的代碼量
- 把復雜的操作分割成多個小操作連續調用
Swift
的特性決定了其在鏈式調用上的先天優勢,但是有沒有辦法讓OC
也能擁有這種鏈式調用的特性呢?答案是毫無疑問的,block
是一種非常優秀的機制,允許我們使用點語法的方式調用屬性block
其他要求
實現鏈式調用做法并不復雜,但是符合這些要求會讓你用起來更加得心應手。譬如:
- 對
block
有過足夠深的使用和了解 - 對
retain cycle
深惡痛疾,網上很多教程實際上存在著循環引用的問題 - 升級到
Xcode8.3
以上的版本,理由無他,加強了屬性block
調用的代碼聯想
其中第三點是筆者最推崇的要求,要知道8.3
之前的鏈式封裝多多少少吃了不少代碼聯想的苦頭
丑陋的數據源
UITableView
是個非常牛逼的控件,但是對于開發者而言也并不是那么的友善,甚至有些丑陋。實現一次tableView
的代理起碼要有以下代碼:
#pragma mark - UITableViewDataSource
- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)section {
return self.dataSource.count;
}
- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: @"reuseIdentifier"];
/// configure cell
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView: (UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath {
/// do something
}
Protocol
是一種非常優雅的設計方案,這點是毋庸置疑的。但即便再優雅的代碼設計,在重復的代碼面前,也會讓人感到丑陋、厭倦。
block
相較起代理,是一種更加強大的機制。比前者更解耦更簡潔,當然兩者的優劣比較不是本文要討論的問題,通過block
調用的返回對象,大可以給NSArray
實現這樣的代碼:
NSArray * dataArr = result[@"data"];
self.models = dataArr.filter(^BOOL(NSDictionary * dict) {
return [dict[@"status"] isEqualToString: @"1"];
}).map(^id(NSDictionary * dict) {
return [[Model alloc] initWithDict: dict];
});
雖然代碼簡潔性上仍然差了Swift
一籌,但是相較起原執行代碼邏輯性跟可讀性都強了不少,這部分的代碼詳見由淺至深學習block
鏈式數據源的實現
由于誰都可能是UITableView
的數據源對象,那么最粗暴的做法是為NSObject
提供一個category
用來實現相關的數據源并且動態添加屬性。但是作為有追求的攻城獅,我們有追求,要優雅,所以這種方式直接cut
掉
UITableView
的繁雜代碼一直是熱議的話題之一,在不同的代碼架構中有不同的解決方案,比如MVVM
中封裝一個專門的VM
來統一處理這部分邏輯。因此可以提供一個對象作為實際數據源對象跟UITableView
之間的中介
。由中介
實現相關協議方法,然后從實際數據源對象方獲取數據
中介被我命名為
LXDTableViewProtocolHelper
,為了保證能夠生成方法鏈,所有的屬性block
應當返回中介對象:
@class LXDTableViewProtocolHelper;
typedef LXDTableViewProtocolHelper *(^TVNumberOfSections)(NSInteger(^)(void));
typedef LXDTableViewProtocolHelper *(^TVRowsNumberAtSection)(NSInteger(^)(NSInteger section));
typedef LXDTableViewProtocolHelper *(^TVDidSelectedCellHandle)(void(^)(NSIndexPath * index));
typedef LXDTableViewProtocolHelper *(^TVConfigureCell)(void(^)(__kindof UITableViewCell * cell, NSIndexPath * index));
typedef LXDTableViewProtocolHelper *(^TVBindAndRegister)(UITableView * tableView, Class cellCls, BOOL isNib, NSString * reuseIdentifier);
@interface LXDTableViewProtocolHelper : NSObject<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, readonly) TVConfigureCell configurateCell;
@property (nonatomic, readonly) TVBindAndRegister bindTableView;
@property (nonatomic, readonly) TVNumberOfSections sectionsNumber;
@property (nonatomic, readonly) TVRowsNumberAtSection rowsNumber;
@property (nonatomic, readonly) TVDidSelectedCellHandle didSelectedCell;
@end
使用只讀屬性修飾block
之后我們只需重寫getter
方法返回對應的處理就行了,拿rowsNumber
做個例子,按照網上很多教程都會這么寫:
- (TVRowsNumberAtSection)rowsNumber {
return ^LXDTableViewProtocolHelper *(NSInteger(^numberOfRowsInSection)(NSInteger section)) {
self.numberOfRowsInSection = numberOfRowsInSection;
return self;
};
}
但是實際上這個返回的block
是__NSMallocBlock__
類型的,這意味著這種做法會讓中介被引用。當然筆者沒去測試中介是否能正確釋放而是直接采用__weak
做法,感興趣的讀者可以重寫dealloc
來檢測。最后tableView
的協議方法就能被這樣實現:
- (void)configureTableViewProtocol {
WeakDefine
self.listHelper.bindTableView(_list, [UITableViewCell class], NO, @"cell").rowsNumber(^NSInteger(NSInteger section) {
return weakself.models.count;
}).configurateCell(^(SingleTitleCell * cell, NSIndexPath * index) {
cell.titleLabel.text = weakself.models[index.row];
}).didSelectedCell(^(NSIndexPath *index) {
[weakself updateInfoWithModel: weakself.models[index.row]];
});
}
更進一步
將協議相關方法抽離之后,不免要持有LXDTableViewProtocolHelper
的實例對象,這樣難免有些不友善。通過category
的方式對UITableView
添加相關的接口來生成并綁定一個helper
是一個不錯的選擇:
@interface UITableView (LXDProtocolConfigure)
- (void)configureHelper: (void(^)(LXDTableViewProtocolHelper * helper))configuration;
@end
- (void)configureHelper: (void(^)(LXDTableViewProtocolHelper * helper))configuration {
LXDTableViewProtocolHelper * helper = [LXDTableViewProtocolHelper new];
configuration(helper);
self.helper = helper;
}
使用associate
相關函數動態綁定helper
,由于后者對前者的屬性修飾為assign
或者weak
也不會導致循環引用的發生,這樣通過下面的代碼就完成了數據源的抽離:
[_dishesList configureHelper: ^(LXDTableViewProtocolHelper * _Nonnull helper) {
helper.bindTableView(_dishesList, [KMCDishesCell class], YES, @"cell").rowsNumber(^NSInteger(NSInteger section) {
return 0;
}).configurateCell(^(__kindof UITableViewCell *cell, NSIndexPath *index) {
}).didSelectedCell(^(NSIndexPath *index) {
});
}];
更多
得益于強大的block
,即便OC
沒有Swift
那么優雅的高階函數,依舊能夠實現讓代碼緊湊已讀,當然也會提高debug
的難度。除了將數據源鏈式之外,你還可以嘗試把網絡請求進行封裝,做成鏈式處理,比如筆者的請求代碼:
Get(Component(@"user/getUserInfo", nil)).then(^(NSDictionary * result) {
/// request success
}).failed(^(NSError * error) {
/// request failed
}).start();