ReactiveCocoa學(xué)習(xí)筆記(三):響應(yīng)式和函數(shù)響應(yīng)式編程

上一篇談了談我自己對函數(shù)式編程的理解。這篇文章會講到,響應(yīng)式編程,函數(shù)響應(yīng)式編程這些又是個(gè)啥,以及我們?yōu)槭裁匆褂盟鼈儭?/p>

響應(yīng)式編程

對于響應(yīng)式編程,我沒有找到比這篇文章更為生動詳盡的文章了,因此這里大部分是翻譯自原文,加上了一些我自己的思考。

輸入和輸出

本質(zhì)上來說,我們構(gòu)建應(yīng)用時(shí),都是在做一件事情:等待一些事件的發(fā)生,來提供一些信息作為輸入。我們根據(jù)這些輸入的信息,進(jìn)行某些處理,生成特定的結(jié)果并輸出。

輸入可以是多種多樣的:「用戶點(diǎn)擊了一個(gè)按鈕」是一種輸入;「服務(wù)器有數(shù)據(jù)返回了」是一種輸入;某個(gè)方法的回調(diào)是一種輸入;或者某個(gè)對象的某個(gè)屬性的變化也可以是一種輸入??粗苎凼旃?,我們每天都在和這些輸入打交道:

///
// delegate
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
}

// block based callbacks
[TCAPI getCategoriesOnComplete:^(NSArray *objects, HTTPOperation *operation, NSError *error) {
}];

// target action
- (IBAction)buttonAction:(id)sender {
}

// timers
[NSTimer scheduledTimerWithTimeInterval:.1 target:self 
                               selector:@selector(spinIt:) userInfo:nil repeats:YES];
                               
// KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
                        change:(NSDictionary *)change context:(void *)context {
}

這些都是Objective-C提供的通信機(jī)制,來給我們傳達(dá)一些輸入信息:某個(gè)事件發(fā)生了,怎么處理你看著辦吧。(題外話:objc.io的這篇文章對于這些通信機(jī)制講的很好)

而我們的輸出也是各種各樣的:我們可以將一些信息保存在本地;或者我們可以通過網(wǎng)絡(luò)協(xié)議將一些信息保存在服務(wù)器上;當(dāng)然,對于移動端app來說,我們最主要的輸出,還是根據(jù)情況去更新UI界面,以便展示新的信息給用戶。

線性編程(Linear Programming)

然而,麻煩的事情來了:我們的每個(gè)輸出,幾乎不會只和一個(gè)輸入相關(guān):當(dāng)收到一個(gè)用戶點(diǎn)擊事件的輸入時(shí),我們需要更新一個(gè)UI界面,但是界面的更新往往也依賴于服務(wù)器的數(shù)據(jù)返回,或是之前用戶的其他操作;更麻煩的是,我們的輸入和輸出是異步的:我們的輸出和輸入在時(shí)間順序上是分離的。

造成這個(gè)麻煩的原因是:我們傳統(tǒng)的編程處理輸入和輸出的方式是線性的(Linear Programming),比如下面這段代碼:


可以看到,我們的每段代碼在時(shí)間順序上都是一段彼此獨(dú)立的的時(shí)間范圍。即使是異步的操作,比如各種callback,也不過是讓我們在時(shí)間軸中間插入一段代碼去執(zhí)行。(這里沒有說到多線程。然而即使是多線程,也只是在另一個(gè)線程上開辟了一條新的時(shí)間軸,開始一段新的線性編程的故事……)

現(xiàn)在讓我們來看看上面說的麻煩事兒吧——當(dāng)我們接收到某個(gè)輸入事件的時(shí)候,我們往往需要作出相應(yīng)的輸出,這個(gè)輸出往往不僅取決于當(dāng)前的輸入,而且是和時(shí)間軸上處于前面的輸入造成的結(jié)果相關(guān)的:

哎呀用戶點(diǎn)擊這個(gè)「菜單」按鈕了,這個(gè)時(shí)候應(yīng)當(dāng)展示出下拉菜單了。但是,首先要確認(rèn)的是,用戶當(dāng)前是否已經(jīng)登錄了呢?這個(gè)菜單所需要的數(shù)據(jù)是否已經(jīng)從服務(wù)器拿到了呢?用戶之前點(diǎn)過這個(gè)按鈕了么?現(xiàn)在是應(yīng)當(dāng)展示這個(gè)菜單還是收起這個(gè)菜單呢?……

相信我們對這樣的邏輯也是再熟悉不過了,我們每天都在這樣寫代碼(手動滑稽)。我們的程序像個(gè)傻瓜金魚,只有7秒鐘的記憶。這樣說太夸張了,其實(shí)我們的程序連1毫秒的記憶也沒有:)。我們只好在有輸入到來的時(shí)候,回過頭再去check一遍之前時(shí)間軸上發(fā)生的事情,檢查一些必要的信息,然后做出輸出。


好,回憶一下我們是怎么去做到的呢?用什么去追蹤時(shí)間軸上前面發(fā)生的故事的呢?我們也很無奈啊,我們只好引入了一個(gè)又一個(gè)的「狀態(tài)」。

狀態(tài)(State)

什么是「狀態(tài)」?「狀態(tài)」是程序運(yùn)行中的參數(shù)的記錄,是程序“現(xiàn)在長啥樣”的描述。

@property (nonatomic, assign) BOOL userIsLoggedIn;
@property (nonatomic, assign) BOOL menuDataIsLoaded;
@property (nonatomic, assign) BOOL isMenuShowing;
...

這些是為了解決上面那個(gè)惱人的問題所需要記錄的狀態(tài)。當(dāng)時(shí)間軸上有事件發(fā)生的時(shí)候,我們?nèi)ジ逻@些狀態(tài);如果某個(gè)輸出需要用到這些信息,我們再去檢查這些property當(dāng)前的值。我們手動去追蹤程序的狀態(tài),在各個(gè)必要的地方去更新它們,然后在一個(gè)名為xxxUpdate的方法中,寫一些復(fù)雜的判斷邏輯來根據(jù)這些狀態(tài)給出我們的輸出:

// a central function that checks all our states and generates the appropriate output
- (void) checkAndUpdateMenuStatus {
    if (self.menuShouldBeShowing && !self.isMenuShowing 
        && self.menuDataIsLoaded && self.userIsLoggedIn) {
        [self showMenu];
    } else if (!self.menuShouldBeShowing && self.isMenuShowing) {
        [self hideMenu];
    }
}

// sets initial states and sets up our notification observation
- (void) viewDidLoad {
    // Set initial states
    // Let's assume you can't get to this page without being logged in
    self.userIsLoggedIn = YES;
    self.isMenuShowing = NO;
    self.menuDataIsLoaded = NO;
    self.menuShouldBeShowing = NO;
    // Need to handle in case the user logs out while on this page
    [[NSNotificationCenter defaultCenter] addObserverForName:kUserLoggedOutNotification 
                                                      object:nil 
                                                       queue:nil 
                                                  usingBlock:^(NSNotification *note) {
        self.userIsLoggedIn = NO;
        [self checkAndUpdateMenuStatus];
    }];
    // set the initial state (somewhat unnecessary since our menu starts hidden
    // but a good safety check)
    [self checkAndUpdateMenuStatus];
}

// Loads the menu data from the network
- (void) loadMenuData {
    [TCPAPI fetchUserMenuData onComplete:^(NSArray *objects, NSError *error) {
        [self hideLoadingView];
        if(!error) {
            self.menuDataIsLoaded = YES;
            [self checkAndUpdateMenuStatus];
        }
    }];
}

// handles showing and hiding a loading view
- (void) startLoadingView {
    if(self.isLoadingShowing) return;
    self.isLoadingShowing = YES;
    // do work to show loading view
}
- (void) hideLoadingView {
    if(!self.isLoadingShowing) return;
    self.isLoadingShowing = NO;
    // do work to hide loading view
}
- (void) showMenu {
    // show menu
    self.isMenuShowing = YES;
}
- (void) hideMenu {
    // hide menu
    self.isMenuShowing = NO;
}
- (IBAction) userTappedMenuButton:(UIButton *menuButton) {
    // kick off loading of our menu data lazily if it isn't loaded yet
    if(!self.menuDataIsLoaded) {
        [self loadMenuData];
    }
    self.menuShouldBeShowing = !self.menuShouldBeShowing;
    [self checkAndUpdateMenuStatus];
}

(心累……原文作者你平時(shí)是在看著我寫程序嗎,還是說天下程序猿都是一樣的傻的可愛)

上面列舉的僅僅是更新一個(gè)UI所需要的狀態(tài)。糟糕的是,當(dāng)我們需要新的信息的時(shí)候,我們往往會不假思索地再添上一個(gè)property,畢竟已經(jīng)形成了肌肉記憶了。慢慢地,我們的代碼里充滿了這些property,以及狀態(tài)判斷的if...else...邏輯。如果有一個(gè)地方出了bug,我們得慢慢去找,哪個(gè)環(huán)節(jié)讓我們親愛的狀態(tài)出了問題。更可怕的是,狀態(tài)帶來的復(fù)雜度是隨著狀態(tài)數(shù)量增加呈指數(shù)級增長的——上面3種狀態(tài)便能帶來2^3種情況,前提還是這3種狀態(tài)都是一個(gè)BOOL值……

響應(yīng)式編程(Reactive Programming)

既然狀態(tài)這么不好,那我們可不可以不要它了呢?聰明的猿們想到了種種方法,讓計(jì)算機(jī)來幫我追蹤和記錄這些狀態(tài)。而我們的工作是在時(shí)間軸的開始,就向計(jì)算機(jī)解釋清楚:我需要哪些輸入信息才能做出一個(gè)特定的輸出,對于一些輸入,我需要做出什么輸出,剩下的事情,就交給計(jì)算機(jī)去做啦。

最常見的例子就是我們的「AutoLayout」:我們向計(jì)算機(jī)說道:“嗯,這個(gè)頁面放在這個(gè)頁面里,它的高度是父頁面的一半,上邊距為10dp,左右居中展示”。然后就哦啦。計(jì)算機(jī)會在父頁面的大小和布局發(fā)生改變的時(shí)候,幫我們?nèi)フ{(diào)整子頁面的大小和位置,而不需要我們在各個(gè)地方手動去寫一堆setFrame:方法。

這就是響應(yīng)式編程(Reactive Programming):我們代碼里,只是說明了各個(gè)事件(輸入)的關(guān)系,以及它們相應(yīng)的輸出。當(dāng)這些事件(輸入)發(fā)生的時(shí)候,計(jì)算機(jī)根據(jù)我們的說明,去進(jìn)行恰當(dāng)?shù)捻憫?yīng)?!笭顟B(tài)」依然是存在的,只不過我們將它們托付給了計(jì)算機(jī)去處理。響應(yīng)式編程處理了時(shí)間軸上輸入和輸出的異步問題,讓我們輕裝上陣,對付各種各樣的業(yè)務(wù)邏輯。

移動app時(shí)代,隨著UI元素越來越多,用戶交互越來越復(fù)雜,處理越來越頻繁,需要的實(shí)時(shí)性也越來越高,這也是響應(yīng)式編程越來越受到開發(fā)者們的青睞的原因吧。

函數(shù)響應(yīng)式編程

響應(yīng)式編程給我們帶來了許多的好處,Cocoa框架中也為我們提供了不少響應(yīng)式編程的支持,例如Autolayout,KVO等等。但是,有沒有可能更進(jìn)一步呢?

上一篇文章講到,函數(shù)式編程中,可以將「數(shù)據(jù)」和「副作用」等封裝成一個(gè)monad,然后就可以盡享函數(shù)式編程的鏈?zhǔn)骄幊痰慕z滑體驗(yàn)了。那如果將我們響應(yīng)式編程中的「輸入」和處理它們的「異步」的邏輯,抽象成一個(gè)monad呢?那么,我們將可以使用鏈?zhǔn)秸Z法和各種強(qiáng)大的函數(shù)式編程的工具,處理各種「輸入」,以及讓「輸入」在函數(shù)式的“管道”中經(jīng)過一步步地處理,最終成為我們需要的「輸出」。

沒錯,這就是函數(shù)響應(yīng)式編程的魅力了!它將一個(gè)隨時(shí)間變化的值抽象成一個(gè)流,并通過monad使其可以利用到函數(shù)式編程的強(qiáng)大工具,最終讓我們可以方便直觀地處理各種「輸入」和「輸出」的異步處理邏輯。

Functional reactive programming (FRP) is a programming paradigm for reactive programming (asynchronous dataflow programming) using the building blocks of functional programming (e.g. map, reduce, filter). -- Wikipedia

函數(shù)響應(yīng)式編程&函數(shù)式響應(yīng)式編程

網(wǎng)絡(luò)上的教程都說,ReactiveCocoa(以及RXSwift)是一個(gè)函數(shù)式響應(yīng)式的編程框架,而沒有說是“函數(shù)響應(yīng)式框架”,讓人傻傻分不清楚。這是為什么呢?

在上文中,其實(shí)強(qiáng)調(diào)了函數(shù)響應(yīng)式中抽象出的值流是隨時(shí)間連續(xù)變化的,其抽象稱為「behaviors」;而像ReactiveCocoa(或是RXSwift)這類框架是應(yīng)用于主要處理人機(jī)交互的移動軟件的,它抽象出的「輸入」流是時(shí)間軸上離散的一個(gè)個(gè)事件,稱為「Event」。這就是兩者的區(qū)別所在。

其實(shí)我個(gè)人覺得,這種區(qū)分在實(shí)際的應(yīng)用中對于我們來說并不重要,ReactiveCocoaGithub主頁也介紹自己為「Streams of values over time」。重要的是能夠理解函數(shù)響應(yīng)式編程的思想,這樣,在使用類似框架的時(shí)候,才能做到知其然并知其所以然。


Reference

Why Reactive(Cocoa)?

The introduction to Reactive Programming you've been missing

stackoverflow - What is functional reactive programming

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容