從知道ReactiveCocoa開始就發現對這個庫有不同的聲音,上次參加<T>技術沙龍時唐巧對在專案中已全面使用FRP的程式碼家提出為什麼這種程式設計模型出現了這麼長時間怎麼像ReactiveCocoa這種完全按FRP編寫的庫沒能夠流行起來這個問題。對這個問題的回答一般都是門檻高,解決方法就是培訓和通過熟悉以前的程式碼來快速入門。其實在我學習的過程中也發現確實會有這個問題,不過就算是有這樣那樣問題使得ReactiveCocoa這樣的庫沒法大面積使用起來,也不能錯失學習這種程式設計思想的機會。
如果不用這樣的庫,能不能將這種庫的程式設計思想融入專案中,發揮出其優勢呢?答案是肯定的。
FRP全稱Function Reactive Programming,從名稱就能夠看出來這個模型關鍵就是Function Programming和Reactive Programming的結合。那麼就先從函數語言程式設計說起。說函數語言程式設計前先聊聊鏈式程式設計,先看看一個開源Alert控制元件的標頭檔案裡定義的介面方法的寫法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * 自定義樣式的alertView * */ + (instancetype)showAlertWithTitle:(NSString *)title message:(NSString *)message completion:(PXAlertViewCompletionBlock)completion cancelTitle:(NSString *)cancelTitle otherTitles:(NSString *)otherTitles, ... NS_REQUIRES_NIL_TERMINATION; /* * @param otherTitles Must be a NSArray containing type NSString, or set to nil for no otherTitles. */ + (instancetype)showAlertWithTitle:(NSString *)title contentView:(UIView *)view secondTitle:(NSString *)secondTitle message:(NSString *)message cancelTitle:(NSString *)cancelTitle otherTitles:(NSArray *)otherTitles btnStyle:(BOOL)btnStyle completion:(PXAlertViewCompletionBlock)completion; |
庫裡還有更多這樣的組合,這麼寫是沒有什麼問題,無非是為了更方便組合使用而囉嗦了點,但是如果現在要新增一個AttributeString,那麼所有組合介面都需要修改,每次呼叫介面方法如果不需要用Attribuite的地方還要去設定nil,這樣會很不易於擴充套件。下面舉個上報日誌介面的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@interface SMLogger : NSObject //初始化 + (SMLogger *)create; //可選設定 - (SMLogger *)object:(id)obj; //object物件記錄 - (SMLogger *)message:(NSString *)msg; //描述 - (SMLogger *)classify:(SMProjectClassify)classify; //分類 - (SMLogger *)level:(SMLoggerLevel)level; //級別 //最後需要執行這個方法進行儲存,什麼都不設定也會記錄檔名,函式名,行數等資訊 - (void)save; @end //巨集 FOUNDATION_EXPORT void SMLoggerDebugFunc(DCProjectClassify classify, DCLoggerLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(3,4); //debug方式列印日誌,不會上報 #define SMLoggerDebug(frmt, ...) \ do { SMLoggerDebugFunc(SMProjectClassifyNormal,DCLoggerLevelDebug,frmt, ##__VA_ARGS__);} while(0) //簡單的上報日誌 #define SMLoggerSimple(frmt, ...) \ do { SMLoggerDebugFunc(SMProjectClassifyNormal,SMLoggerLevelDebug,frmt, ##__VA_ARGS__);} while(0) //自定義classify和level的日誌,可上報 #define SMLoggerCustom(classify,level,frmt, ...) \ do { SMLoggerDebugFunc(classify,level,frmt, ##__VA_ARGS__);} while(0) |
從這個標頭檔案可以看出,對介面所需的引數不用將各種組合一一定義,只需要按照需要組合即可,而且做這個日誌介面時發現後續維護過程中會增加越來越多的功能和需要更多的input資料。比如每條日誌新增應用生命週期唯一編號,產品線每次切換唯一編號這樣需要在特定場景需要新增的input支援。採用這種方式會更加易於擴充套件。寫的時候會是[[[[DCLogger create] message:@”此處必改”] classify:DCProjectClassifyTradeHome] save]; 這樣,對於不是特定場所較通用的場景可以使用巨集來定義,內部實現還是按照前者的來實現,看起來是[DCLogger loggerWithMessage:@”此處必改”];,這樣就能夠同時滿足常用場景和特殊場景的呼叫需求。
有了鏈式程式設計這種易於擴充套件方式的程式設計方式再來建構函式式程式設計,函式程式設計主要思路就是用有輸入輸出的函式作為引數將運算過程儘量寫成一系列巢狀的函式呼叫,下面我構造一個需求來看看函數語言程式設計的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
typedef NS_ENUM(NSUInteger, SMStudentGender) { SMStudentGenderMale, SMStudentGenderFemale }; typedef BOOL(^SatisfyActionBlock)(NSUInteger credit); @interface SMStudent : NSObject @property (nonatomic, strong) SMCreditSubject *creditSubject; @property (nonatomic, assign) BOOL isSatisfyCredit; + (SMStudent *)create; - (SMStudent *)name:(NSString *)name; - (SMStudent *)gender:(SMStudentGender)gender; - (SMStudent *)studentNumber:(NSUInteger)number; //積分相關 - (SMStudent *)sendCredit:(NSUInteger(^)(NSUInteger credit))updateCreditBlock; - (SMStudent *)filterIsASatisfyCredit:(SatisfyActionBlock)satisfyBlock; @end |
這個例子中,sendCredit的block函式引數會處理當前的積分這個資料然後返回給SMStudent記錄下來,filterIsASatisfyCredit的block函式引數會處理是否達到合格的積分判斷返回是或否的BOOL值給SMStudent記錄下來。實現程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//present self.student = [[[[[SMStudent create] name:@"ming"] gender:SMStudentGenderMale] studentNumber:345] filterIsASatisfyCredit:^BOOL(NSUInteger credit){ if (credit >= 70) { self.isSatisfyLabel.text = @"合格"; self.isSatisfyLabel.textColor = [UIColor redColor]; return YES; } else { self.isSatisfyLabel.text = @"不合格"; return NO; } }]; @weakify(self); [[self.testButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) { @strongify(self); [self.student sendCredit:^NSUInteger(NSUInteger credit) { credit += 5; NSLog(@"current credit %lu",credit); [self.student.creditSubject sendNext:credit]; return credit; }]; }]; [self.student.creditSubject subscribeNext:^(NSUInteger credit) { NSLog(@"第一個訂閱的credit處理積分%lu",credit); self.currentCreditLabel.text = [NSString stringWithFormat:@"%lu",credit]; if (credit < 30) { self.currentCreditLabel.textColor = [UIColor lightGrayColor]; } else if(credit < 70) { self.currentCreditLabel.textColor = [UIColor purpleColor]; } else { self.currentCreditLabel.textColor = [UIColor redColor]; } }]; [self.student.creditSubject subscribeNext:^(NSUInteger credit) { NSLog(@"第二個訂閱的credit處理積分%lu",credit); if (!(credit > 0)) { self.currentCreditLabel.text = @"0"; self.isSatisfyLabel.text = @"未設定"; } }]; |
每次按鈕點選都會增加5個積分,達到70個積分就算合格了。上面的例子裡可以看到一個對每次積分變化有不同的觀察者處理的操作程式碼,這裡並沒有使用ReactiveCocoa裡的訊號,而是自己實現了一個特定的積分的類似訊號的物件,方法名也用的是一樣的。實現這個物件也是用的函數語言程式設計方式。下面我的具體的實現程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
@interface SMCreditSubject : NSObject typedef void(^SubscribeNextActionBlock)(NSUInteger credit); + (SMCreditSubject *)create; - (SMCreditSubject *)sendNext:(NSUInteger)credit; - (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block; @end @interface SMCreditSubject() @property (nonatomic, assign) NSUInteger credit; @property (nonatomic, strong) SubscribeNextActionBlock subscribeNextBlock; @property (nonatomic, strong) NSMutableArray *blockArray; @end @implementation SMCreditSubject + (SMCreditSubject *)create { SMCreditSubject *subject = [[self alloc] init]; return subject; } - (SMCreditSubject *)sendNext:(NSUInteger)credit { self.credit = credit; if (self.blockArray.count > 0) { for (SubscribeNextActionBlock block in self.blockArray) { block(self.credit); } } return self; } - (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block { if (block) { block(self.credit); } [self.blockArray addObject:block]; return self; } #pragma mark - Getter - (NSMutableArray *)blockArray { if (!_blockArray) { _blockArray = [NSMutableArray array]; } return _blockArray; } |
Demo地址:https://github.com/ming1016/RACStudy
主要思路就是subscribeNext時將引數block的實現輸入新增到一個陣列中,sendNext時記錄輸入的積分,同時遍歷那個記錄subscribeNext的block的陣列使那些block再按照新積分再實現一次輸入,達到更新積分通知多個subscriber來實現新值的效果。
除了block還可以將每次sendNext的積分放入一個陣列記錄每次的積分變化,在RAC中的Signal就是這樣處理的,如下圖,這樣新加入的subscirber能夠讀取到積分變化歷史記錄。
所以不用ReactiveCocoa庫也能夠按照函數語言程式設計方式改造現有專案達到同樣的效果。
上面的例子也能夠看出FRP的另一個響應式程式設計的特性。說響應式程式設計之前可以先看看我之前關於解耦的那篇文章裡的Demohttps://github.com/ming1016/DecoupleDemo,裡面使用了Model作為連線檢視,請求儲存和控制器之間的紐帶,通過KVO使它們能夠通過Model的屬性來相互監聽來避免它們之間的相互依賴達到解耦的效果。
像上面的例子那樣其實也能夠達到同樣的效果,建立一個Model然後通過各個Subject來貫穿檢視層和資料層進行send值和多subscribe值的處理。
瞭解了這種程式設計模型,再去了解下ReactiveCocoa使用的三種設計模式就能夠更容易的將它學以致用了,下面配上這三種貫穿ReactiveCocoa的設計模式,看這些圖裡的方法名是不是很眼熟。