FRP全稱Function Reactive Programming,從名稱就能夠看出來這個模型關鍵就是Function Programming和Reactive Programming的結合。那么就先從函數式編程說起。說函數式編程前先聊聊鏈式編程,先看看一個開源Alert控件的頭文件里定義的接口方法的寫法。
/*
* 自定義樣式的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,這樣會很不易于擴展。下面舉個上報日志接口的例子。
@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:@”此處必改”];,這樣就能夠同時滿足常用場景和特殊場景的調用需求。
有了鏈式編程這種易于擴展方式的編程方式再來構造函數式編程,函數編程主要思路就是用有輸入輸出的函數作為參數將運算過程盡量寫成一系列嵌套的函數調用,下面我構造一個需求來看看函數式編程的例子。
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記錄下來。實現代碼如下
//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里的信號,而是自己實現了一個特定的積分的類似信號的對象,方法名也用的是一樣的。實現這個對象也是用的函數式編程方式。下面我的具體的實現代碼
@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的設計模式,看這些圖里的方法名是不是很眼熟。
ReactiveCocoa里面還有很多可以學習的地方,比如宏的運用,可以看看sunnyxx的那篇《Reactive Cocoa Tutorial [1] = 神奇的Macros》http://blog.sunnyxx.com/2014/03/06/rac_1_macros/