本文是 "RxJava 沉思錄" 系列的最後一篇分享。本系列全部分享:java
咱們在本系列開篇中,曾經留了一個問題:RxJava 是否可讓咱們的代碼更簡潔?做爲本系列的最後一篇分享,咱們將詳細地探討這個問題。承接前面兩篇 「時間維度」 和 「空間維度」 的探討,咱們首先從 RxJava 的維度 開始提及。git
在前面兩篇分享中,咱們解讀了不少案例,最終得出結論:RxJava 經過 Observable
這個統一的接口,對其相關的事件,在空間維度和事件維度進行從新組織,來簡化咱們平常的事件驅動編程。github
前文中提到:編程
有了
Observable
之後的 RxJava 纔剛剛插上了想象力的翅膀。api
RxJava 全部想象力的基石和源泉在於 Observable
這個統一的接口,有了它,配合咱們各類各樣的操做符,才能夠在時間空間維度玩出花樣。網絡
咱們回想一下原先咱們基於 Callback 的編程範式:數據結構
btn.setOnClickListener(v -> {
// handle click event
})
複製代碼
在基於 Callback
的編程範式中,咱們的 Callback
是 沒有維度 的。它只可以 響應孤立的事件,即來一個事件,我處理一個事件。假設同一個事件先後存在依賴關係,或者不一樣事件之間存在依賴關係,不管是時間維度仍是空間維度,若是咱們仍是繼續用 Callback
的方式處理,咱們必然須要新增許多額外的數據結構來保存中間的上下文信息,同時 Callback
自己的邏輯也須要修改,觀察者的邏輯會變得不那麼純粹。架構
可是 RxJava 給咱們的事件驅動型編程帶來了新的思路,RxJava 的 Observable
一會兒把咱們的維度拓展到了時間和空間兩個維度。若是事件與事件間存在依賴關係,原先咱們須要新增的數據結構以及在 Callback 內寫的額外的控制邏輯的代碼,如今均可以不用寫,咱們只須要利用 Observable
的操做符對事件在時間和空間維度進行從新組織,就能夠實現同樣的效果,而觀察者的邏輯幾乎不須要修改。app
因此若是把 RxJava 的編程思想和傳統的面向 Callback 的編程思想進行對比,用一個詞形容的話,那就是 降維打擊。框架
這是我認爲目前大多數與 RxJava 有關的技術分享沒有提到的一個很是重要的點,而且我認爲這纔是 RxJava 最精髓最核心的思想。RxJava 對咱們平常編程最重要的貢獻,就是提高了咱們原先對於事件驅動型編程的思考的維度,給人一種大夢初醒的感受,和這點比起來,所謂的 「鏈式寫法」 這種語法糖什麼的,根本不值一提。
不管是同步仍是異步,咱們平常的事件驅動型編程能夠被當作是一種 「生產者——消費者」 模型:
在異步的狀況下,咱們的代碼能夠被分爲兩大塊,一塊生產事件,一塊消費事件,二者經過 Callback 聯繫起來。而 Callback 是輕量級的,大多數和 Callback 相關的邏輯就僅僅是設置回調和取消設置的回調而已。
若是咱們的項目中引入了 RxJava ,咱們能夠發現,「生產者——消費者」 這個模型中,中間多了一層 RxJava 相關的邏輯層:
而這一層的做用,咱們在以前的討論中已經明確,是用來對生產者產生的事件進行從新組織的。這個架構之下,生產者這一層的變化不會很大,直接受影響的是消費者這一層,因爲 RxJava 這一層對事件進行了「預處理」,消費者這一層代碼會比以前輕不少。同時因爲 RxJava 取代了原先的 Callback 這一層,RxJava 這一層的代碼是會比原先 Callback 這一層更厚。
這麼作還會有什麼其餘的好處呢?首先最直接的好處即是代碼會更易於測試。原先生產者和消費者之間是耦合的,因爲如今引入了 RxJava,生產者和消費者之間沒有直接的耦合關係,測試的時候能夠很方便的對生產者和消費者分開進行測試。好比原先網絡請求相關邏輯,測試就不是很方便,可是若是咱們使用 RxJava 進行解耦之後,觀察者僅僅只是耦合 Observable
這個接口而已,咱們能夠本身手動建立用於測試的 Observable
,這些 Observable
負責發射 Mock 的數據,這樣就能夠很方便的對觀察者的代碼進行測試,而不須要真正的去發起網絡請求。
取消訂閱這個功能也是咱們在觀察者模式中常常用到的一個功能點,尤爲是在 Android 開發領域,因爲 Activity 生命週期的關係,咱們常常須要將網絡請求與 Activity 生命週期綁定,即在 Activity 銷燬的時候取消全部未完成的網絡請求。
常規面向 Callback 的編程方式咱們沒法在觀察者這一層完成取消訂閱這一邏輯,咱們經常須要找到事件生產者這一層才能完成取消訂閱。例如咱們須要取消點擊事件的訂閱時,咱們不得不找到點擊事件產生的源頭,來取消訂閱:
btn.setOnClickListener(null);
複製代碼
然而在 RxJava 的世界裏,取消訂閱這個邏輯終於下放到觀察者這一層了。事件的生產者須要在提供 Observable
的同時,實現當它的觀察者取消訂閱時,它應該實現的邏輯(例如釋放資源);事件的觀察者當訂閱一個 Observable
時,它同時會獲得一個 Disposable
,觀察者但願取消訂閱事件的時候,只須要經過這個接口通知事件生產者便可,徹底不須要了解事件是如何產生的、事件的源頭在哪裏。
至此,生產者和消費者在 RxJava 的世界裏已經完成了完全的解耦。除此之外,RxJava 還提供了好用的線程池,在 生產者——消費者 這個模型裏,咱們經常會要求二者工做在不一樣的線程中,切換線程是剛需,RxJava 徹底考慮到了這一點,而且把切換線程的功能封裝成了 subscribeOn
和 observerOn
兩個操做符,咱們能夠在事件流處理的任什麼時候機隨意切換線程,鑑於這一塊已經有不少資料了,這裏再也不詳細展開。
這一塊不屬於 RxJava 的核心 Feature,可是若是掌握好這塊,可讓咱們使用 RxJava 編程效率大大提高。
咱們舉一個實際的例子,Activity 內發起的網絡請求都須要綁定生命週期,即咱們須要在 Activity 銷燬的時候取消訂閱全部未完成的網絡請求。假設我目前已經能夠得到一個 Observable<ActivityEvent>
, 這是一個能接收到 Activity 生命週期的 Observable
(獲取方法能夠借鑑三方框架 RxLifecycle,或者本身內建一個不可見 Fragment,用來接收生命週期的回調)。
那麼用來保證每個網絡請求都能綁定 Activity 生命週期的代碼應以下所示:
public interface NetworkApi {
@GET("/path/to/api")
Call<List<Photo>> getAllPhotos();
}
public class MainActivity extends Activity {
Observable<ActivityEvent> lifecycle = ...
NetworkApi networkApi = ...
@Override
protected void onCreate(Bundle savedInstanceState) {
// 發起請求同時綁定生命週期
networkApi.getAllPhotos()
.compose(bindToLifecycle())
.subscribe(result -> {
// handle results
});
}
private <T> ObservableTransformer<T, T> bindToLifecycle() {
return upstream -> upstream.takeUntil(
lifecycle.filter(ActivityEvent.DESTROY::equals)
);
}
}
複製代碼
若是您以前沒有接觸過
ObservableTransformer
, 這裏作一個簡單介紹,它一般和compose
操做符一塊兒使用,用來把一個Observable
進行加工、修飾,甚至替換爲另外一個Observable
。
在這裏咱們封裝了一個 bindToLifecycle
方法,它的返回類型是 ObservableTransformer
,在 ObservableTransformer
內部,咱們修飾了原 Observable
, 使其能夠在接收到 Activity 的 DESTROY 事件的時候自動取消訂閱,這個邏輯是由 takeUntil
這個操做符完成的。其實咱們能夠把這個 bindToLifecycle
方法抽取出來,放到公共的工具類,這樣任何的 Activity 內部發起的網絡請求時,都只須要加一行 .compose(bindToLifecycle())
就能夠保證綁定生命週期了,今後不再必擔憂因爲網絡請求引發的內存泄漏和崩潰了。
事實上咱們還能夠有更多玩法, 上面 ObservableTransformer
內部的 upstream
對象,就是一個 Observable
,也就是說能夠調用它的 doOnSubscribe
和 doOnTerminate
方法,咱們能夠在這兩個方法裏實現 Loading 動畫的顯隱:
private <T> ObservableTransformer<T, T> applyLoading() {
return upstream -> upstream
.doOnSubscribe(() -> {
loading.show();
})
.doOnTerminae(() -> {
loading.dismiss();
});
);
}
複製代碼
這樣,咱們的網絡請求只要調用兩個 compose
操做符,就能夠完成生命週期的綁定以及與之對應的 Loading 動畫的顯隱了:
networkApi.getAllPhotos()
.compose(bindToLifecycle())
.compose(applyLoading())
.subscribe(result -> {
// handle results
});
複製代碼
操做符 compose
是 RxJava 給咱們提供的能夠面向 Observable
進行 AOP 的接口,善加利用就能夠幫咱們節省大量的時間和精力。
在前文中,咱們還留了一個問題還沒有解答:RxJava 真的更簡潔嗎?本文中列舉了不少實際的例子,咱們也看到了,從代碼量看,有時候使用 RxJava 的版本比 Callback 的版本更少,有時候二者差很少,有時候 Callback 版本的代碼反而更少。因此咱們可能沒法從代碼量上對二者作出公正的考量,因此咱們須要從其餘方面,例如代碼的閱讀難度、可維護性上去評判了。
首先我想要明確一點,RxJava 是一個 「夾帶了私貨」 的框架,它自己最重要的貢獻是提高了咱們思考事件驅動型編程的維度,可是它與此同時又逼迫咱們去接受了函數式編程。函數式編程在處理集合、列表這些數據結構時相比較指令式編程具備先天的優點,我理解框架的設計者,因爲框架自己提高了咱們對事件思考的維度,那麼不管是時間維度仍是空間維度,一連串發射出來的事件其實就能夠被當作許許多多事件的集合,既然是集合,那確定是使用函數式的風格去處理更加優雅。
原先的時候,咱們接觸的函數式編程只是用於處理靜態的數據,當咱們接觸了 RxJava 以後,發現動態的異步事件組成的集合竟然也可使用函數式編程的方式去處理,我不禁地佩服框架設計者的腦洞大開。事實上,RxJava 不少操做符都是直接照搬函數式編程中處理集合的函數,例如:map
, filter
, flatMap
, reduce
等等。
可是,函數式編程是一把雙刃劍,它也會給你帶來不利的因素,一方面,這意味着你的團隊都須要瞭解函數式編程的思想,另外一方面,函數式的編程風格,意味着代碼會比原先更加抽象。
好比在前面的分享中 「實現一個具備多種類型的 RecyclerView」 這個例子中, combineLatest
這個操做符,完成了原先 onOk()
方法、resultTypes
、responseList
一塊兒配合才完成的任務。雖然原先的版本代碼不夠內聚,不如 RxJava 版本的簡練,可是若是從可閱讀性和可維護性上來看,我認爲原先的版本更好,由於我看到這幾個方法和字段,能夠推測出這段代碼的意圖是什麼,但是若是是 combineLatest
這個操做符,也許我寫的那個時候我知道我是什麼意圖,一旦過一段時間回來看,我對着這個這個 combineLatest
操做符可能就一臉懵逼了,我必須從這個事件流最開始的地方從上往下捋一遍,結合實際的業務邏輯,我才能回想起爲何當時要用 combineLatest
這個操做符了。
再舉一個例子,在 「社交軟件上消息的點贊與取消點贊」 這個例子中,若是我不是對這種「把事件流中相鄰事件進行比較」的編碼方式瞭如指掌的話,一旦隔一段時間,我再次面對這幾個 debounce
、zipWith
、flatMap
操做符時,我可能會懷疑本身寫的代碼。本身寫的代碼都如此,更況且大多數狀況下咱們須要面對別人寫的代碼。
這就是爲何 RxJava 寫出的代碼會更加抽象,由於 RxJava 的操做符是咱們平時處理業務邏輯時經常使用方法的高度抽象。 combineLatest
是對咱們本身寫的 onOk
等方法的抽象,zipWith
幫咱們省略了原本要寫的中間變量,debounce
操做符替代了咱們原本要寫的計時器邏輯。從功能上來說二者實際上是等價的,只不過 RxJava 給咱們提供了高度抽象凝練,更加具備普適性的寫法。
在本文前半部分,咱們說到過,有的人認爲 RxJava 是簡潔的,而有的人的見解則徹底相反,這件事的本質在於你們對 簡潔 的指望不一樣,大多數人認爲的簡潔指得是代碼簡單好理解,而高度抽象的代碼是不知足這一點的,因此不少人最後發現理解抽象的 RxJava 代碼須要花更多的時間,反而不 「簡潔」 。認爲 RxJava 簡潔的人所認爲的 簡潔 更像是那種相似數學概念上的那種 簡潔,這是由於函數式編程的抽象風格與數學更接近。咱們舉個例子,你們都知道牛頓第二定律,但是你知道牛頓在《天然哲學的數學原理》上發表牛頓二定律的時候的原始公式表示是什麼樣的嗎:
公式中的 p 表示動量,這是牛頓所認爲的"簡潔",而咱們大多數人認爲簡單好記的版本是 「物體的加速度等於施加在物體上的力除以物體的質量」。
這就是爲何,我在前面提早下了那個結論:對於大多數人,RxJava 不等於簡潔,有時候甚至是更難以理解的代碼以及更低的項目可維護性。
而目前大多數我看到的有關 RxJava 的技術文章舉例說明的所謂 「邏輯簡潔」 或者是 「隨着程序邏輯的複雜性提升,依然可以保持簡潔」 的例子大多數都是不恰當的。一方面他們僅僅停留在 Callback 的維度,舉那種依次執行的異步任務的例子,徹底沒有點到 RxJava 對處理問題的維度的提高這一點;二是舉的那些例子實在難以使人信服,至少我並無以爲那些例子用了 RxJava 相比 Callback 有多麼大的提高。
綜上所述,咱們能夠得出這樣的結論,RxJava 是一個思想優秀的框架,並且是那種在工程領域少見的帶有學院派氣息和理想主義色彩的框架,他是一種新型的事件驅動型編程範式。 RxJava 最重要的貢獻,就是提高了咱們原先對於事件驅動型編程的思考的維度,容許咱們能夠從時間和空間兩個維度去從新組織事件。
此外,RxJava 好在哪,真的和「觀察者模式」、「鏈式編程」、「線程池」、「解決 Callback Hell」等等關係沒那麼大,這些特性相比上面總結的而言,都是微不足道的。
我是不會用「簡潔」、「邏輯簡潔」、「清晰」、「優雅」 那樣空洞的字眼去描述 RxJava 這個框架的,這確實是一個學習曲線陡峭的框架,並且若是團隊成員總體對函數式編程認識不夠深入的話,項目的後期維護也是充滿風險的。
固然我但願你也不要所以被我嚇到,我我的是推崇 RxJava 的,在我本人蔘與的項目中已經大規模鋪開使用了 RxJava。本文前面提到過:
RxJava 是一種新的 事件驅動型 編程範式,它以異步爲切入點,試圖一統 同步 和 異步 的世界。
在我參與的項目中,我已經漸漸能感覺到這種 「天下大同」 的感受了。這也是爲何我能聽到不少人都會說 「一旦用了 RxJava 就很難再放棄了」。
也許這時候你會問我,到底推不推薦你們使用 RxJava ?我認爲是這樣,若是你認爲在你的項目裏,Callback 模式已經不能知足你的平常須要,事件之間存在複雜的依賴關係,你須要從更高的維度空間去從新思考你的問題,或者說你須要常常在時間或者空間維度上去從新組織你的事件,那麼恭喜你, RxJava 正是爲你打造的;若是你認爲在你的項目裏,目前使用 Callback 模式已經很好知足了你的平常開發須要,簡單的業務邏輯也根本玩不出什麼新花樣,那麼 RxJava 就是不適合你的。
(完)
本文屬於 "RxJava 沉思錄" 系列,歡迎閱讀本系列的其餘分享:
若是您對個人技術分享感興趣,歡迎關注個人我的公衆號:麻瓜日記,不按期更新原創技術分享,謝謝!:)