原文鏈接:http://www.bignerdranch.com/blog/what-is-functional-reactive-programming/
函數響應式編程(FRP)為解決現代編程問題提供了全新的視角。一旦理解它,可以極大地簡化你的項目,特別是處理嵌套回調的異步事件,復雜的列表過濾和變換,或者時間相關問題。
我將盡量跳過對函數響應式編程學院式的解釋(網絡上已經有很多),并重點從實用的角度幫你理解什么是函數響應式編程,以及工作中怎么應用它。本文將圍繞函數響應式編程的一個具體實現RxJava, 它可用于Java和Android。
開始
我們以一個真實的例子來開始講解函數響應式編程怎么提高我們代碼的可讀性。我們的任務是通過查詢GitHub的API, 首先獲取用戶列表,然后請求每個用戶的詳細信息。這個過程包括兩個web 服務端點:https://api.github.com/users - 獲取用戶列表; https://api.github.com/users/{username} -獲取特定用戶的詳細信息,例如https://api.github.com/users/mutexkid。
舊的風格
下面例子你可能已經很熟悉了:它調用web service,使用回調接口將成功的結果傳遞給下一個web service請求,同時定義另一個成功回調,然后發起下一個web service請求。你可以看到,這會導致兩層嵌套的回調:
//The "Nested Callbacks" Way
public void fetchUserDetails() {
//first, request the users...
mService.requestUsers(new Callback<GithubUsersResponse>() {
@Override
public void success(final GithubUsersResponse githubUsersResponse,
final Response response) {
Timber.i(TAG, "Request Users request completed");
final List<GithubUserDetail> githubUserDetails = new ArrayList<GithubUserDetail>();
//next, loop over each item in the response
for (GithubUserDetail githubUserDetail : githubUsersResponse) {
//request a detail object for that user
mService.requestUserDetails(githubUserDetail.mLogin,
new Callback<GithubUserDetail>() {
@Override
public void success(GithubUserDetail githubUserDetail,
Response response) {
Log.i("User Detail request completed for user : " + githubUserDetail.mLogin);
githubUserDetails.add(githubUserDetail);
if (githubUserDetails.size() == githubUsersResponse.mGithubUsers.size()) {
//we've downloaded'em all - notify all who are interested!
mBus.post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
}
}
@Override
public void failure(RetrofitError error) {
Log.e(TAG, "Request User Detail Failed!!!!", error);
}
});
}
}
@Override
public void failure(RetrofitError error) {
Log.e(TAG, "Request User Failed!!!!", error);
}
});
}
盡管這不是最差的代碼-至少它是異步的,因此在等待每個請求完成的時候不會阻塞-但由于代碼復雜(增加更多層次的回調代碼復雜度將呈指數級增長)因此遠非理想的代碼。當我們不可避免要修改代碼時(在前面的web service調用中,我們依賴前一次的回調狀態,因此它不適用于模塊化或者修改要傳遞給下一個回調的數據)也遠非容易的工作。我們親切的稱這種情況為“回調地獄”。
RxJava的方式
下面讓我們看看使用RxJava如何實現相同的功能:
public void rxFetchUserDetails() {
//request the users
mService.rxRequestUsers().concatMap(Observable::from)
.concatMap((GithubUser githubUser) ->
//request the details for each user
mService.rxRequestUserDetails(githubUser.mLogin)
)
//accumulate them as a list
.toList()
//define which threads information will be passed on
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
//post them on an eventbus
.subscribe(githubUserDetails -> {
EventBus.getDefault().post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
});
}
如你所見,使用函數響應式編程模型我們完全擺脫了回調,并最終得到了更短小的程序。讓我們從函數響應式編程的基本定義開始慢慢解釋到底發生了什么,并逐漸理解上面的代碼,這些代碼托管在GitHub上面。
從根本上講,函數響應式編程是在觀察者模式的基礎上,增加對Observables發送的數據流進行操縱和變換的功能。在上面的例子中,Observables是我們的數據流通所在的管道。
回顧一下,觀察者模式包含兩個角色:一個Observable和一個或者多個Observers。Observable發送事件,而Observer訂閱和接收這些事件。在上面的例子中,.subscribe()函數用于給Observable添加一個Observer,并創建一個請求。
構建一個Observable管道
對Observable管道的每個操作都將返回一個新的Observable,這個新的Observable的內容要么和操作前相同,要么就是經過轉換的。這種方式使得我們可以對任務進行分解,并把事件流分解成小的操作,接著把這些Observables拼接起來從而完成更復雜的行為或者重用管道中的每個獨立的單元。我們對Observable的每個方法調用會被加入到總的管道中以便我們的數據在其中流動。
下面首先讓我們從搭建一個Observable開始,來看一個具體的例子:
Observable<String> sentenceObservable = Observable.from(“this”, “is”, “a”, “sentence”);
這樣我們就定義好了管道的第一個部分:Observable。在其中流通的數據是一個字符串序列。首先要認識到的是這是沒有實現任何功能的非阻塞代碼,僅僅定義了我們想要完成什么事情。Observable只有在我們“訂閱”它之后才會開始工作,也就是說給它注冊一個Observer之后。
Observable.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});
到這一步Observable才會發送由每個獨立的Observable的from()函數添加的數據塊。管道會持續發送Observables直到所有Observables都被處理完成。
變換數據流
現在我們得到正在發送的字符串流,我們可以按照需要對這些數據流進行變換,并建立更復雜的行為。
Observable<String> sentenceObservable = Observable.from(“this”, “is”, “a”, “sentence”);
sentenceObservable.map(new Func1<String, String>() {
@Override
public String call(String s) {
return s.toUpperCase() + " ";
}
})
.toList()
.map(new Func1<List<String>, String>() {
@Override
public String call(List<String> strings) {
Collections.reverse(strings);
return strings.toString();
}
})
//subscribe to the stream of Observables
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});
一旦Observable被訂閱了,我們會得到“SENTENCE A IS THIS”。上面調用的.map函數接受Func1類的對象,該類有兩個范型類型參數:一個是輸入類型(前一個Observable的內容),另一個是輸出類型(在這個例子中,是一個經過大寫轉換,格式化并用一個新的Observable實例包裝的字符串,最終被傳遞給下一個函數)。如你所見,我們通過可重用的管道組合實現更復雜的功能。
上面的例子中,我們還可以使用Java8的lambda表達式來進一步簡化代碼:
Observable.just("this", "is", "a", "sentence").map(s -> s.toUpperCase() + " ").toList().map(strings -> {
Collections.reverse(strings);
return strings.toString();
});
在subscribe函數中,我們傳遞Action1類對象作為參數,并以String類型作為范型參數。這定義了訂閱者的行為,當被觀察者發送最后一個事件后,處理后的字符串就被接收到了。這是.subscribe()函數最簡單的重載形式(參見https://github.com/ReactiveX/RxJava/wiki/Observable#establishing-subscribers 可以看到更復雜的函數重載簽名)。
這個例子展示了變換函數.map()和聚合函數.toList(),在操縱數據流的能力方面這僅僅是冰山一角(所有可用的數據流操作函數可見https://github.com/ReactiveX/RxJava/wiki), 但它顯示了基本概念:在函數響應式編程中,我們可以通過實現了數據轉換或者數據操作功能的管道中獨立的單元來轉換數據流。根據需要我們可以在其他由Observables組成的管道復用這些獨立的單元。通過把這些Observable單元拼接在一起,我們可以組成更復雜的特性,但同時保持它們作為易于理解和可修改的可組合邏輯小單元。
使用Scheduler管理線程
在web service例子中,我們展示了如何使用RxJava發起網絡請求。我們談及轉換,聚合和訂閱Observable數據流,但我們沒有談及Observable數據流的web請求是怎樣實現異步的。
這就屬于FRP編程模型如何調用Scheduler的范疇了-該策略定義了Observable流事件在哪個線程中發生,以及訂閱者在哪個線程消費Observable的處理結果。在web service例子中,我們希望請求在后臺線程中進行,而訂閱行為發生在主線程中,因此我們如下定義:
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
//post them on an eventbus
.subscribe(githubUserDetails -> {
EventBus.getDefault().post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
});
Observable.subscribeOn(Scheduler scheduler)函數指定Observable的工作需要在指定的Scheduler線程中執行。Observable.observeOn(Scheduler scheduler)指定Observable在哪個Scheduler線程觸發訂閱者們的onNext(),onCompleted(),和onError()函數,并調用Observable的observeOn()函數,傳遞正確的Scheduler給它。
下面是可能會用到Scheduler:
- Schedulers.computation():用于計算型工作例如事件循環和回調處理,不要在I/O中使用這個函數(應該使用Schedulers.io()函數);
Schedulers.from(executor):使用指定的Executor作為Scheduler;
Schedulers.immediate():在當前線程中立即開始執行任務;
Schedulers.io():用于I/O密集型工作例如阻塞I/O的異步操作,這個調度器由一個會隨需增長的線程池支持;對于一般的計算工作,使用Schedulers.computation();
Schedulers.newThread():為每個工作單元創建一個新的線程;
Schedulers.test():用于測試目的,支持單元測試的高級事件;
Schedulers.trampoline():在當前線程中的工作放入隊列中排隊,并依次操作。
通過設置observeOn和subscribeOn調度器,我們定義了網絡請求使用哪個線程(Schedulers.newThread())。
下一步
我們已經在本文中涵蓋了很多基礎內容,到這里你應該對函數響應式編程如何工作有了很好的認識。請查看并理解本文介紹的工程,它托管在GitHub上面,閱讀RxJava文檔并檢出rxjava-koans工程,以測試驅動的方式掌握函數響應式編程范型。