RxJava 沉思錄(四):總結

轉自https://juejin.im/post/5b8f536c5188255c352d3528

我們在本系列開篇中,曾經留了一個問題:RxJava 是否可以讓我們的代碼更簡潔?作爲本系列的最後一篇分享,我們將詳細地探討這個問題。承接前面兩篇 “時間維度” 和 “空間維度” 的探討,我們首先從 RxJava 的維度 開始說起。

RxJava 的維度

在前面兩篇分享中,我們解讀了很多案例,最終得出結論:RxJava 通過 Observable 這個統一的接口,對其相關的事件,在空間維度和事件維度進行重新組織,來簡化我們日常的事件驅動編程

前文中提到:

有了 Observable 以後的 RxJava 纔剛剛插上了想象力的翅膀。

RxJava 所有想象力的基石和源泉在於 Observable 這個統一的接口,有了它,配合我們各種各樣的操作符,纔可以在時間空間維度玩出花樣。

我們回想一下原先我們基於 Callback 的編程範式:

btn.setOnClickListener(v -> {
    // handle click event
})
複製代碼

在基於 Callback 的編程範式中,我們的 Callback沒有維度 的。它只能夠 響應孤立的事件,即來一個事件,我處理一個事件。假設同一個事件前後存在依賴關係,或者不同事件之間存在依賴關係,無論是時間維度還是空間維度,如果我們還是繼續用 Callback 的方式處理,我們必然需要新增許多額外的數據結構來保存中間的上下文信息,同時 Callback 本身的邏輯也需要修改,觀察者的邏輯會變得不那麼純粹。

但是 RxJava 給我們的事件驅動型編程帶來了新的思路,RxJava 的 Observable 一下子把我們的維度拓展到了時間和空間兩個維度。如果事件與事件間存在依賴關係,原先我們需要新增的數據結構以及在 Callback 內寫的額外的控制邏輯的代碼,現在都可以不用寫,我們只需要利用 Observable 的操作符對事件在時間和空間維度進行重新組織,就可以實現一樣的效果,而觀察者的邏輯幾乎不需要修改。

所以如果把 RxJava 的編程思想和傳統的面向 Callback 的編程思想進行對比,用一個詞形容的話,那就是 降維打擊

這是我認爲目前大多數與 RxJava 有關的技術分享沒有提到的一個非常重要的點,並且我認爲這纔是 RxJava 最精髓最核心的思想。RxJava 對我們日常編程最重要的貢獻,就是提升了我們原先對於事件驅動型編程的思考的維度,給人一種大夢初醒的感覺,和這點比起來,所謂的 “鏈式寫法” 這種語法糖什麼的,根本不值一提。

生產者消費者模式中 RxJava 扮演的角色

無論是同步還是異步,我們日常的事件驅動型編程可以被看成是一種 “生產者——消費者” 模型:

Callback

 

 

在異步的情況下,我們的代碼可以被分爲兩大塊,一塊生產事件,一塊消費事件,兩者通過 Callback 聯繫起來。而 Callback 是輕量級的,大多數和 Callback 相關的邏輯就僅僅是設置回調和取消設置的回調而已。

如果我們的項目中引入了 RxJava ,我們可以發現,“生產者——消費者” 這個模型中,中間多了一層 RxJava 相關的邏輯層:

RxJava

 

 

而這一層的作用,我們在之前的討論中已經明確,是用來對生產者產生的事件進行重新組織的。這個架構之下,生產者這一層的變化不會很大,直接受影響的是消費者這一層,由於 RxJava 這一層對事件進行了“預處理”,消費者這一層代碼會比之前輕很多。同時由於 RxJava 取代了原先的 Callback 這一層,RxJava 這一層的代碼是會比原先 Callback 這一層更厚。

這麼做還會有什麼其他的好處呢?首先最直接的好處便是代碼會更易於測試。原先生產者和消費者之間是耦合的,由於現在引入了 RxJava,生產者和消費者之間沒有直接的耦合關係,測試的時候可以很方便的對生產者和消費者分開進行測試。比如原先網絡請求相關邏輯,測試就不是很方便,但是如果我們使用 RxJava 進行解耦以後,觀察者僅僅只是耦合 Observable 這個接口而已,我們可以自己手動創建用於測試的 Observable,這些 Observable 負責發射 Mock 的數據,這樣就可以很方便的對觀察者的代碼進行測試,而不需要真正的去發起網絡請求。

取消訂閱與 Scheduler

取消訂閱這個功能也是我們在觀察者模式中經常用到的一個功能點,尤其是在 Android 開發領域,由於 Activity 生命週期的關係,我們經常需要將網絡請求與 Activity 生命週期綁定,即在 Activity 銷燬的時候取消所有未完成的網絡請求。

常規面向 Callback 的編程方式我們無法在觀察者這一層完成取消訂閱這一邏輯,我們常常需要找到事件生產者這一層才能完成取消訂閱。例如我們需要取消點擊事件的訂閱時,我們不得不找到點擊事件產生的源頭,來取消訂閱:

btn.setOnClickListener(null);
複製代碼

然而在 RxJava 的世界裏,取消訂閱這個邏輯終於下放到觀察者這一層了。事件的生產者需要在提供 Observable 的同時,實現當它的觀察者取消訂閱時,它應該實現的邏輯(例如釋放資源);事件的觀察者當訂閱一個 Observable 時,它同時會得到一個 Disposable ,觀察者希望取消訂閱事件的時候,只需要通過這個接口通知事件生產者即可,完全不需要了解事件是如何產生的、事件的源頭在哪裏。

至此,生產者和消費者在 RxJava 的世界裏已經完成了徹底的解耦。除此以外,RxJava 還提供了好用的線程池,在 生產者——消費者 這個模型裏,我們常常會要求兩者工作在不同的線程中,切換線程是剛需,RxJava 完全考慮到了這一點,並且把切換線程的功能封裝成了 subscribeOnobserverOn 兩個操作符,我們可以在事件流處理的任何時機隨意切換線程,鑑於這一塊已經有很多資料了,這裏不再詳細展開。

面向 Observable 的 AOP:compose 操作符

這一塊不屬於 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,也就是說可以調用它的 doOnSubscribedoOnTerminate 方法,我們可以在這兩個方法裏實現 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 真的更簡潔嗎?本文中列舉了很多實際的例子,我們也看到了,從代碼量看,有時候使用 RxJava 的版本比 Callback 的版本更少,有時候兩者差不多,有時候 Callback 版本的代碼反而更少。所以我們可能無法從代碼量上對兩者做出公正的考量,所以我們需要從其他方面,例如代碼的閱讀難度、可維護性上去評判了。

首先我想要明確一點,RxJava 是一個 “夾帶了私貨” 的框架,它本身最重要的貢獻是提升了我們思考事件驅動型編程的維度,但是它與此同時又逼迫我們去接受了函數式編程。函數式編程在處理集合、列表這些數據結構時相比較指令式編程具有先天的優勢,我理解框架的設計者,由於框架本身提升了我們對事件思考的維度,那麼無論是時間維度還是空間維度,一連串發射出來的事件其實就可以被看成許許多多事件的集合,既然是集合,那肯定是使用函數式的風格去處理更加優雅。

 

 

 

原先的時候,我們接觸的函數式編程只是用於處理靜態的數據,當我們接觸了 RxJava 之後,發現動態的異步事件組成的集合居然也可以使用函數式編程的方式去處理,我不由地佩服框架設計者的腦洞大開。事實上,RxJava 很多操作符都是直接照搬函數式編程中處理集合的函數,例如:map, filter, flatMap, reduce 等等。

但是,函數式編程是一把雙刃劍,它也會給你帶來不利的因素,一方面,這意味着你的團隊都需要了解函數式編程的思想,另一方面,函數式的編程風格,意味着代碼會比原先更加抽象。

比如在前面的分享中 “實現一個具有多種類型的 RecyclerView” 這個例子中, combineLatest 這個操作符,完成了原先 onOk() 方法、resultTypesresponseList 一起配合才完成的任務。雖然原先的版本代碼不夠內聚,不如 RxJava 版本的簡練,但是如果從可閱讀性和可維護性上來看,我認爲原先的版本更好,因爲我看到這幾個方法和字段,可以推測出這段代碼的意圖是什麼,可是如果是 combineLatest 這個操作符,也許我寫的那個時候我知道我是什麼意圖,一旦過一段時間回來看,我對着這個這個 combineLatest 操作符可能就一臉懵逼了,我必須從這個事件流最開始的地方從上往下捋一遍,結合實際的業務邏輯,我才能回想起爲什麼當時要用 combineLatest 這個操作符了。

再舉一個例子,在 “社交軟件上消息的點贊與取消點贊” 這個例子中,如果我不是對這種“把事件流中相鄰事件進行比較”的編碼方式瞭如指掌的話,一旦隔一段時間,我再次面對這幾個 debouncezipWithflatMap 操作符時,我可能會懷疑自己寫的代碼。自己寫的代碼都如此,更何況大多數情況下我們需要面對別人寫的代碼。

這就是爲什麼 RxJava 寫出的代碼會更加抽象,因爲 RxJava 的操作符是我們平時處理業務邏輯時常用方法的高度抽象combineLatest 是對我們自己寫的 onOk 等方法的抽象,zipWith 幫我們省略了本來要寫的中間變量,debounce 操作符替代了我們本來要寫的計時器邏輯。從功能上來講兩者其實是等價的,只不過 RxJava 給我們提供了高度抽象凝練,更加具有普適性的寫法。

在本文前半部分,我們說到過,有的人認爲 RxJava 是簡潔的,而有的人的看法則完全相反,這件事的本質在於大家對 簡潔 的期望不同,大多數人認爲的簡潔指得是代碼簡單好理解,而高度抽象的代碼是不滿足這一點的,所以很多人最後發現理解抽象的 RxJava 代碼需要花更多的時間,反而不 “簡潔” 。認爲 RxJava 簡潔的人所認爲的 簡潔 更像是那種類似數學概念上的那種 簡潔,這是因爲函數式編程的抽象風格與數學更接近。我們舉個例子,大家都知道牛頓第二定律,可是你知道牛頓在《自然哲學的數學原理》上發表牛頓二定律的時候的原始公式表示是什麼樣的嗎:

 

Newton's second law

 

 

公式中的 p 表示動量,這是牛頓所認爲的"簡潔",而我們大多數人認爲簡單好記的版本是 “物體的加速度等於施加在物體上的力除以物體的質量”。

這就是爲什麼,我在前面提前下了那個結論:對於大多數人,RxJava 不等於簡潔,有時候甚至是更難以理解的代碼以及更低的項目可維護性。

而目前大多數我看到的有關 RxJava 的技術文章舉例說明的所謂 “邏輯簡潔” 或者是 “隨着程序邏輯的複雜性提高,依然能夠保持簡潔” 的例子大多數都是不恰當的。一方面他們僅僅停留在 Callback 的維度,舉那種依次執行的異步任務的例子,完全沒有點到 RxJava 對處理問題的維度的提升這一點;二是舉的那些例子實在難以令人信服,至少我並沒有覺得那些例子用了 RxJava 相比 Callback 有多麼大的提升。

RxJava 是否適合你的項目

綜上所述,我們可以得出這樣的結論,RxJava 是一個思想優秀的框架,而且是那種在工程領域少見的帶有學院派氣息和理想主義色彩的框架,他是一種新型的事件驅動型編程範式。 RxJava 最重要的貢獻,就是提升了我們原先對於事件驅動型編程的思考的維度,允許我們可以從時間和空間兩個維度去重新組織事件。

此外,RxJava 好在哪,真的和“觀察者模式”、“鏈式編程”、“線程池”、“解決 Callback Hell”等等關係沒那麼大,這些特性相比上面總結的而言,都是微不足道的。

我是不會用“簡潔”、“邏輯簡潔”、“清晰”、“優雅” 那樣空洞的字眼去描述 RxJava 這個框架的,這確實是一個學習曲線陡峭的框架,而且如果團隊成員整體對函數式編程認識不夠深刻的話,項目的後期維護也是充滿風險的。

當然我希望你也不要因此被我嚇到,我個人是推崇 RxJava 的,在我本人蔘與的項目中已經大規模鋪開使用了 RxJava。本文前面提到過:

RxJava 是一種新的 事件驅動型 編程範式,它以異步爲切入點,試圖一統 同步異步 的世界。

在我參與的項目中,我已經漸漸能感受到這種 “天下大同” 的感覺了。這也是爲什麼我能聽到很多人都會說 “一旦用了 RxJava 就很難再放棄了”。

也許這時候你會問我,到底推不推薦大家使用 RxJava ?我認爲是這樣,如果你認爲在你的項目裏,Callback 模式已經不能滿足你的日常需要,事件之間存在複雜的依賴關係,你需要從更高的維度空間去重新思考你的問題,或者說你需要經常在時間或者空間維度上去重新組織你的事件,那麼恭喜你, RxJava 正是爲你打造的;如果你認爲在你的項目裏,目前使用 Callback 模式已經很好滿足了你的日常開發需要,簡單的業務邏輯也根本玩不出什麼新花樣,那麼 RxJava 就是不適合你的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章