RxJava 沉思錄(四):總結

prototypez發表於1970-01-01

本文是 "RxJava 沉思錄" 系列的最後一篇分享。本系列所有分享:

我們在本系列開篇中,曾經留了一個問題: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 之後,發現動態的非同步事件組成的集合居然也可以使用函數語言程式設計的方式去處理,我不由地佩服框架設計者的腦洞大開。事實上,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 就是不適合你的。

(完)

本文屬於 "RxJava 沉思錄" 系列,歡迎閱讀本系列的其他分享:


如果您對我的技術分享感興趣,歡迎關注我的個人公眾號:麻瓜日記,不定期更新原創技術分享,謝謝!:)

RxJava 沉思錄(四):總結

相關文章