從ReactiveCocoa中能學到什么?不用此庫也能學以致用

從知道ReactiveCocoa開始就發現對這個庫有不同的聲音,上次參加<T>技術沙龍時唐巧對在項目中已全面使用FRP的代碼家提出為什么這種編程模型出現了這么長時間怎么像ReactiveCocoa這種完全按FRP編寫的庫沒能夠流行起來這個問題。對這個問題的回答一般都是門檻高,解決方法就是培訓和通過熟悉以前的代碼來快速入門。其實在我學習的過程中也發現確實會有這個問題,不過就算是有這樣那樣問題使得ReactiveCocoa這樣的庫沒法大面積使用起來,也不能錯失學習這種編程思想的機會。

如果不用這樣的庫,能不能將這種庫的編程思想融入項目中,發揮出其優勢呢?答案是肯定的。

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能夠讀取到積分變化歷史記錄。

演示 - 7.png

所以不用ReactiveCocoa庫也能夠按照函數式編程方式改造現有項目達到同樣的效果。

上面的例子也能夠看出FRP的另一個響應式編程的特性。說響應式編程之前可以先看看我之前關于解耦的那篇文章里的Demohttps://github.com/ming1016/DecoupleDemo,里面使用了Model作為連接視圖,請求存儲和控制器之間的紐帶,通過KVO使它們能夠通過Model的屬性來相互監聽來避免它們之間的相互依賴達到解耦的效果。

演示 - 6.png

像上面的例子那樣其實也能夠達到同樣的效果,創建一個Model然后通過各個Subject來貫穿視圖層和數據層進行send值和多subscribe值的處理。

了解了這種編程模型,再去了解下ReactiveCocoa使用的三種設計模式就能夠更容易的將它學以致用了,下面配上這三種貫穿ReactiveCocoa的設計模式,看這些圖里的方法名是不是很眼熟。

演示 - 9.png

演示 - 10.png

演示 - 11.png

ReactiveCocoa里面還有很多可以學習的地方,比如宏的運用,可以看看sunnyxx的那篇《Reactive Cocoa Tutorial [1] = 神奇的Macros》http://blog.sunnyxx.com/2014/03/06/rac_1_macros/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容

  • 今天5月19日,星期5,距離高考還有18天。今天天氣特別涼爽,空氣中透著一種冷冷的感覺,應是附近某地下了大雨吧。一...
    耘心閱讀 210評論 0 0
  • 以往都不覺得元旦就是跨年,總覺得除夕守歲才是正在一年初始,大概是因為今年正好在16年底差不多還清了除房貸以外的債務...
    冒泡的小魚閱讀 387評論 0 0
  • 一只青蛙,呱呱呱! 小鴨子很惱怒,嫌它吵,一腳把它踢進了井里! 于是,它便在井里成長,習慣了那圓形的小小的窩,也忘...
    遠曉閱讀 532評論 2 2