本文是 "RxJava 沉思錄" 系列的最后一篇分享占哟。本系列所有分享:
我們在本系列開篇中解虱,曾經(jīng)留了一個問題:RxJava 是否可以讓我們的代碼更簡潔?作為本系列的最后一篇分享,我們將詳細(xì)地探討這個問題射窒。承接前面兩篇 “時間維度” 和 “空間維度” 的探討,我們首先從 RxJava 的維度 開始說起。
RxJava 的維度
在前面兩篇分享中冶忱,我們解讀了很多案例,最終得出結(jié)論:RxJava 通過 Observable
這個統(tǒng)一的接口境析,對其相關(guān)的事件囚枪,在空間維度和事件維度進(jìn)行重新組織派诬,來簡化我們?nèi)粘5氖录?qū)動編程。
前文中提到:
有了
Observable
以后的 RxJava 才剛剛插上了想象力的翅膀链沼。
RxJava 所有想象力的基石和源泉在于 Observable
這個統(tǒng)一的接口默赂,有了它,配合我們各種各樣的操作符括勺,才可以在時間空間維度玩出花樣缆八。
我們回想一下原先我們基于 Callback 的編程范式:
btn.setOnClickListener(v -> {
// handle click event
})
在基于 Callback
的編程范式中,我們的 Callback
是 沒有維度 的疾捍。它只能夠 響應(yīng)孤立的事件奈辰,即來一個事件,我處理一個事件乱豆。假設(shè)同一個事件前后存在依賴關(guān)系奖恰,或者不同事件之間存在依賴關(guān)系,無論是時間維度還是空間維度宛裕,如果我們還是繼續(xù)用 Callback
的方式處理瑟啃,我們必然需要新增許多額外的數(shù)據(jù)結(jié)構(gòu)來保存中間的上下文信息,同時 Callback
本身的邏輯也需要修改揩尸,觀察者的邏輯會變得不那么純粹蛹屿。
但是 RxJava 給我們的事件驅(qū)動型編程帶來了新的思路,RxJava 的 Observable
一下子把我們的維度拓展到了時間和空間兩個維度岩榆。如果事件與事件間存在依賴關(guān)系蜡峰,原先我們需要新增的數(shù)據(jù)結(jié)構(gòu)以及在 Callback 內(nèi)寫的額外的控制邏輯的代碼,現(xiàn)在都可以不用寫朗恳,我們只需要利用 Observable
的操作符對事件在時間和空間維度進(jìn)行重新組織,就可以實現(xiàn)一樣的效果载绿,而觀察者的邏輯幾乎不需要修改粥诫。
所以如果把 RxJava 的編程思想和傳統(tǒng)的面向 Callback 的編程思想進(jìn)行對比,用一個詞形容的話崭庸,那就是 降維打擊怀浆。
這是我認(rèn)為目前大多數(shù)與 RxJava 有關(guān)的技術(shù)分享沒有提到的一個非常重要的點,并且我認(rèn)為這才是 RxJava 最精髓最核心的思想怕享。RxJava 對我們?nèi)粘>幊套钪匾呢暙I(xiàn)执赡,就是提升了我們原先對于事件驅(qū)動型編程的思考的維度,給人一種大夢初醒的感覺函筋,和這點比起來沙合,所謂的 “鏈?zhǔn)綄懛ā?這種語法糖什么的,根本不值一提跌帐。
生產(chǎn)者消費(fèi)者模式中 RxJava 扮演的角色
無論是同步還是異步首懈,我們?nèi)粘5氖录?qū)動型編程可以被看成是一種 “生產(chǎn)者——消費(fèi)者” 模型:
在異步的情況下绊率,我們的代碼可以被分為兩大塊,一塊生產(chǎn)事件究履,一塊消費(fèi)事件滤否,兩者通過 Callback 聯(lián)系起來。而 Callback 是輕量級的最仑,大多數(shù)和 Callback 相關(guān)的邏輯就僅僅是設(shè)置回調(diào)和取消設(shè)置的回調(diào)而已藐俺。
如果我們的項目中引入了 RxJava ,我們可以發(fā)現(xiàn)泥彤,“生產(chǎn)者——消費(fèi)者” 這個模型中欲芹,中間多了一層 RxJava 相關(guān)的邏輯層:
而這一層的作用,我們在之前的討論中已經(jīng)明確全景,是用來對生產(chǎn)者產(chǎn)生的事件進(jìn)行重新組織的耀石。這個架構(gòu)之下,生產(chǎn)者這一層的變化不會很大爸黄,直接受影響的是消費(fèi)者這一層滞伟,由于 RxJava 這一層對事件進(jìn)行了“預(yù)處理”,消費(fèi)者這一層代碼會比之前輕很多炕贵。同時由于 RxJava 取代了原先的 Callback 這一層梆奈,RxJava 這一層的代碼是會比原先 Callback 這一層更厚。
這么做還會有什么其他的好處呢称开?首先最直接的好處便是代碼會更易于測試亩钟。原先生產(chǎn)者和消費(fèi)者之間是耦合的,由于現(xiàn)在引入了 RxJava鳖轰,生產(chǎn)者和消費(fèi)者之間沒有直接的耦合關(guān)系清酥,測試的時候可以很方便的對生產(chǎn)者和消費(fèi)者分開進(jìn)行測試。比如原先網(wǎng)絡(luò)請求相關(guān)邏輯蕴侣,測試就不是很方便焰轻,但是如果我們使用 RxJava 進(jìn)行解耦以后,觀察者僅僅只是耦合 Observable
這個接口而已昆雀,我們可以自己手動創(chuàng)建用于測試的 Observable
辱志,這些 Observable
負(fù)責(zé)發(fā)射 Mock 的數(shù)據(jù),這樣就可以很方便的對觀察者的代碼進(jìn)行測試狞膘,而不需要真正的去發(fā)起網(wǎng)絡(luò)請求揩懒。
取消訂閱與 Scheduler
取消訂閱這個功能也是我們在觀察者模式中經(jīng)常用到的一個功能點,尤其是在 Android 開發(fā)領(lǐng)域挽封,由于 Activity 生命周期的關(guān)系已球,我們經(jīng)常需要將網(wǎng)絡(luò)請求與 Activity 生命周期綁定,即在 Activity 銷毀的時候取消所有未完成的網(wǎng)絡(luò)請求。
常規(guī)面向 Callback 的編程方式我們無法在觀察者這一層完成取消訂閱這一邏輯和悦,我們常常需要找到事件生產(chǎn)者這一層才能完成取消訂閱退疫。例如我們需要取消點擊事件的訂閱時,我們不得不找到點擊事件產(chǎn)生的源頭鸽素,來取消訂閱:
btn.setOnClickListener(null);
然而在 RxJava 的世界里褒繁,取消訂閱這個邏輯終于下放到觀察者這一層了。事件的生產(chǎn)者需要在提供 Observable
的同時馍忽,實現(xiàn)當(dāng)它的觀察者取消訂閱時棒坏,它應(yīng)該實現(xiàn)的邏輯(例如釋放資源);事件的觀察者當(dāng)訂閱一個 Observable
時遭笋,它同時會得到一個 Disposable
坝冕,觀察者希望取消訂閱事件的時候,只需要通過這個接口通知事件生產(chǎn)者即可瓦呼,完全不需要了解事件是如何產(chǎn)生的喂窟、事件的源頭在哪里。
至此央串,生產(chǎn)者和消費(fèi)者在 RxJava 的世界里已經(jīng)完成了徹底的解耦磨澡。除此以外,RxJava 還提供了好用的線程池质和,在 生產(chǎn)者——消費(fèi)者 這個模型里稳摄,我們常常會要求兩者工作在不同的線程中,切換線程是剛需饲宿,RxJava 完全考慮到了這一點厦酬,并且把切換線程的功能封裝成了 subscribeOn
和 observerOn
兩個操作符,我們可以在事件流處理的任何時機(jī)隨意切換線程瘫想,鑒于這一塊已經(jīng)有很多資料了仗阅,這里不再詳細(xì)展開。
面向 Observable 的 AOP:compose 操作符
這一塊不屬于 RxJava 的核心 Feature国夜,但是如果掌握好這塊减噪,可以讓我們使用 RxJava 編程效率大大提升。
我們舉一個實際的例子支竹,Activity 內(nèi)發(fā)起的網(wǎng)絡(luò)請求都需要綁定生命周期,即我們需要在 Activity 銷毀的時候取消訂閱所有未完成的網(wǎng)絡(luò)請求鸠按。假設(shè)我目前已經(jīng)可以獲得一個 Observable<ActivityEvent>
, 這是一個能接收到 Activity 生命周期的 Observable
(獲取方法可以借鑒三方框架 RxLifecycle礼搁,或者自己內(nèi)建一個不可見 Fragment,用來接收生命周期的回調(diào))目尖。
那么用來保證每一個網(wǎng)絡(luò)請求都能綁定 Activity 生命周期的代碼應(yīng)如下所示:
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) {
// 發(fā)起請求同時綁定生命周期
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
進(jìn)行加工、修飾饮戳,甚至替換為另一個Observable
豪治。
在這里我們封裝了一個 bindToLifecycle
方法,它的返回類型是 ObservableTransformer
扯罐,在 ObservableTransformer
內(nèi)部负拟,我們修飾了原 Observable
, 使其可以在接收到 Activity 的 DESTROY 事件的時候自動取消訂閱,這個邏輯是由 takeUntil
這個操作符完成的歹河。其實我們可以把這個 bindToLifecycle
方法抽取出來掩浙,放到公共的工具類,這樣任何的 Activity 內(nèi)部發(fā)起的網(wǎng)絡(luò)請求時秸歧,都只需要加一行 .compose(bindToLifecycle())
就可以保證綁定生命周期了厨姚,從此再也不必?fù)?dān)心由于網(wǎng)絡(luò)請求引起的內(nèi)存泄漏和崩潰了。
事實上我們還可以有更多玩法键菱, 上面 ObservableTransformer
內(nèi)部的 upstream
對象谬墙,就是一個 Observable
,也就是說可以調(diào)用它的 doOnSubscribe
和 doOnTerminate
方法经备,我們可以在這兩個方法里實現(xiàn) Loading 動畫的顯隱:
private <T> ObservableTransformer<T, T> applyLoading() {
return upstream -> upstream
.doOnSubscribe(() -> {
loading.show();
})
.doOnTerminae(() -> {
loading.dismiss();
});
);
}
這樣拭抬,我們的網(wǎng)絡(luò)請求只要調(diào)用兩個 compose
操作符,就可以完成生命周期的綁定以及與之對應(yīng)的 Loading 動畫的顯隱了:
networkApi.getAllPhotos()
.compose(bindToLifecycle())
.compose(applyLoading())
.subscribe(result -> {
// handle results
});
操作符 compose
是 RxJava 給我們提供的可以面向 Observable
進(jìn)行 AOP 的接口弄喘,善加利用就可以幫我們節(jié)省大量的時間和精力玖喘。
RxJava 真的讓你的代碼更簡潔?
在前文中蘑志,我們還留了一個問題尚未解答:RxJava 真的更簡潔嗎累奈?本文中列舉了很多實際的例子,我們也看到了急但,從代碼量看澎媒,有時候使用 RxJava 的版本比 Callback 的版本更少,有時候兩者差不多波桩,有時候 Callback 版本的代碼反而更少戒努。所以我們可能無法從代碼量上對兩者做出公正的考量,所以我們需要從其他方面镐躲,例如代碼的閱讀難度储玫、可維護(hù)性上去評判了。
首先我想要明確一點萤皂,RxJava 是一個 “夾帶了私貨” 的框架撒穷,它本身最重要的貢獻(xiàn)是提升了我們思考事件驅(qū)動型編程的維度,但是它與此同時又逼迫我們?nèi)ソ邮芰撕瘮?shù)式編程裆熙。函數(shù)式編程在處理集合端礼、列表這些數(shù)據(jù)結(jié)構(gòu)時相比較指令式編程具有先天的優(yōu)勢禽笑,我理解框架的設(shè)計者,由于框架本身提升了我們對事件思考的維度蛤奥,那么無論是時間維度還是空間維度佳镜,一連串發(fā)射出來的事件其實就可以被看成許許多多事件的集合,既然是集合凡桥,那肯定是使用函數(shù)式的風(fēng)格去處理更加優(yōu)雅蟀伸。
原先的時候,我們接觸的函數(shù)式編程只是用于處理靜態(tài)的數(shù)據(jù)唬血,當(dāng)我們接觸了 RxJava 之后望蜡,發(fā)現(xiàn)動態(tài)的異步事件組成的集合居然也可以使用函數(shù)式編程的方式去處理,我不由地佩服框架設(shè)計者的腦洞大開拷恨。事實上脖律,RxJava 很多操作符都是直接照搬函數(shù)式編程中處理集合的函數(shù),例如:map
, filter
, flatMap
, reduce
等等腕侄。
但是小泉,函數(shù)式編程是一把雙刃劍,它也會給你帶來不利的因素冕杠,一方面微姊,這意味著你的團(tuán)隊都需要了解函數(shù)式編程的思想,另一方面分预,函數(shù)式的編程風(fēng)格兢交,意味著代碼會比原先更加抽象。
比如在前面的分享中 “實現(xiàn)一個具有多種類型的 RecyclerView” 這個例子中笼痹, combineLatest
這個操作符配喳,完成了原先 onOk()
方法、resultTypes
凳干、responseList
一起配合才完成的任務(wù)晴裹。雖然原先的版本代碼不夠內(nèi)聚,不如 RxJava 版本的簡練救赐,但是如果從可閱讀性和可維護(hù)性上來看涧团,我認(rèn)為原先的版本更好,因為我看到這幾個方法和字段经磅,可以推測出這段代碼的意圖是什么泌绣,可是如果是 combineLatest
這個操作符,也許我寫的那個時候我知道我是什么意圖预厌,一旦過一段時間回來看阿迈,我對著這個這個 combineLatest
操作符可能就一臉懵逼了,我必須從這個事件流最開始的地方從上往下捋一遍配乓,結(jié)合實際的業(yè)務(wù)邏輯仿滔,我才能回想起為什么當(dāng)時要用 combineLatest
這個操作符了。
再舉一個例子犹芹,在 “社交軟件上消息的點贊與取消點贊” 這個例子中崎页,如果我不是對這種“把事件流中相鄰事件進(jìn)行比較”的編碼方式了如指掌的話,一旦隔一段時間腰埂,我再次面對這幾個 debounce
飒焦、zipWith
、flatMap
操作符時屿笼,我可能會懷疑自己寫的代碼牺荠。自己寫的代碼都如此,更何況大多數(shù)情況下我們需要面對別人寫的代碼驴一。
這就是為什么 RxJava 寫出的代碼會更加抽象休雌,因為 RxJava 的操作符是我們平時處理業(yè)務(wù)邏輯時常用方法的高度抽象。 combineLatest
是對我們自己寫的 onOk
等方法的抽象肝断,zipWith
幫我們省略了本來要寫的中間變量杈曲,debounce
操作符替代了我們本來要寫的計時器邏輯。從功能上來講兩者其實是等價的胸懈,只不過 RxJava 給我們提供了高度抽象凝練担扑,更加具有普適性的寫法。
在本文前半部分趣钱,我們說到過涌献,有的人認(rèn)為 RxJava 是簡潔的,而有的人的看法則完全相反首有,這件事的本質(zhì)在于大家對 簡潔 的期望不同燕垃,大多數(shù)人認(rèn)為的簡潔指得是代碼簡單好理解,而高度抽象的代碼是不滿足這一點的绞灼,所以很多人最后發(fā)現(xiàn)理解抽象的 RxJava 代碼需要花更多的時間利术,反而不 “簡潔” 。認(rèn)為 RxJava 簡潔的人所認(rèn)為的 簡潔 更像是那種類似數(shù)學(xué)概念上的那種 簡潔低矮,這是因為函數(shù)式編程的抽象風(fēng)格與數(shù)學(xué)更接近印叁。我們舉個例子,大家都知道牛頓第二定律军掂,可是你知道牛頓在《自然哲學(xué)的數(shù)學(xué)原理》上發(fā)表牛頓二定律的時候的原始公式表示是什么樣的嗎:
公式中的 p 表示動量轮蜕,這是牛頓所認(rèn)為的"簡潔",而我們大多數(shù)人認(rèn)為簡單好記的版本是 “物體的加速度等于施加在物體上的力除以物體的質(zhì)量”蝗锥。
這就是為什么跃洛,我在前面提前下了那個結(jié)論:對于大多數(shù)人,RxJava 不等于簡潔终议,有時候甚至是更難以理解的代碼以及更低的項目可維護(hù)性汇竭。
而目前大多數(shù)我看到的有關(guān) RxJava 的技術(shù)文章舉例說明的所謂 “邏輯簡潔” 或者是 “隨著程序邏輯的復(fù)雜性提高葱蝗,依然能夠保持簡潔” 的例子大多數(shù)都是不恰當(dāng)?shù)摹R环矫嫠麄儍H僅停留在 Callback 的維度细燎,舉那種依次執(zhí)行的異步任務(wù)的例子两曼,完全沒有點到 RxJava 對處理問題的維度的提升這一點;二是舉的那些例子實在難以令人信服玻驻,至少我并沒有覺得那些例子用了 RxJava 相比 Callback 有多么大的提升悼凑。
RxJava 是否適合你的項目
綜上所述,我們可以得出這樣的結(jié)論璧瞬,RxJava 是一個思想優(yōu)秀的框架户辫,而且是那種在工程領(lǐng)域少見的帶有學(xué)院派氣息和理想主義色彩的框架,他是一種新型的事件驅(qū)動型編程范式嗤锉。 RxJava 最重要的貢獻(xiàn)渔欢,就是提升了我們原先對于事件驅(qū)動型編程的思考的維度,允許我們可以從時間和空間兩個維度去重新組織事件瘟忱。
此外膘茎,RxJava 好在哪,真的和“觀察者模式”酷誓、“鏈?zhǔn)骄幊獭迸怠ⅰ熬€程池”、“解決 Callback Hell”等等關(guān)系沒那么大盐数,這些特性相比上面總結(jié)的而言棒拂,都是微不足道的。
我是不會用“簡潔”玫氢、“邏輯簡潔”帚屉、“清晰”、“優(yōu)雅” 那樣空洞的字眼去描述 RxJava 這個框架的漾峡,這確實是一個學(xué)習(xí)曲線陡峭的框架攻旦,而且如果團(tuán)隊成員整體對函數(shù)式編程認(rèn)識不夠深刻的話,項目的后期維護(hù)也是充滿風(fēng)險的生逸。
當(dāng)然我希望你也不要因此被我嚇到牢屋,我個人是推崇 RxJava 的,在我本人參與的項目中已經(jīng)大規(guī)模鋪開使用了 RxJava槽袄。本文前面提到過:
RxJava 是一種新的 事件驅(qū)動型 編程范式烙无,它以異步為切入點,試圖一統(tǒng) 同步 和 異步 的世界遍尺。
在我參與的項目中截酷,我已經(jīng)漸漸能感受到這種 “天下大同” 的感覺了。這也是為什么我能聽到很多人都會說 “一旦用了 RxJava 就很難再放棄了”乾戏。
也許這時候你會問我迂苛,到底推不推薦大家使用 RxJava 三热?我認(rèn)為是這樣,如果你認(rèn)為在你的項目里三幻,Callback 模式已經(jīng)不能滿足你的日常需要康铭,事件之間存在復(fù)雜的依賴關(guān)系,你需要從更高的維度空間去重新思考你的問題赌髓,或者說你需要經(jīng)常在時間或者空間維度上去重新組織你的事件,那么恭喜你催跪, RxJava 正是為你打造的锁蠕;如果你認(rèn)為在你的項目里,目前使用 Callback 模式已經(jīng)很好滿足了你的日常開發(fā)需要懊蒸,簡單的業(yè)務(wù)邏輯也根本玩不出什么新花樣荣倾,那么 RxJava 就是不適合你的。
(完)
本文屬于 "RxJava 沉思錄" 系列骑丸,歡迎閱讀本系列的其他分享: