重新理解響應(yīng)式編程

前言

這是前段時間我在公司內(nèi)部Android組的技術(shù)分享會上,以響應(yīng)式編程為主題做的一個專題分享嗓化,反饋還不錯撩鹿,但是也有很多問題睡扬,因此我根據(jù)反饋重新修改和完善了相關(guān)的論述盟蚣,組成一篇文章分享給大家。

研究這個問題的初衷在于目前很多人對于RxJava這種庫卖怜,以及它背后所體現(xiàn)的編程思想了解不多屎开,而網(wǎng)上也很少有人能夠把它講明白,很多時候只能參考網(wǎng)絡(luò)上的一些RxJava項目實踐去學(xué)習(xí)RxJava的使用马靠。但是我始終認(rèn)為奄抽,只有熟悉響應(yīng)式編程的思想,才能更好的使用RxJava這個Rx拓展庫甩鳄。

目前網(wǎng)絡(luò)上中英文的資料對于響應(yīng)式編程的描述有些兩極分化逞度,要么只能將響應(yīng)式的概念解釋清楚,沒有可實踐性妙啃,要么就是從RxJava的定義出發(fā)來解釋響應(yīng)式編程第晰。比如“響應(yīng)式編程就是異步數(shù)據(jù)流編程”這種話,看似抓住了重點彬祖,但是實際上你很難從這個定義中收獲有用的東西。

因此品抽,今天我希望講講響應(yīng)式編程的思想和它的優(yōu)勢储笑,以及怎樣去理解響應(yīng)式編程才能更好的把它融入到我們的編程工作中,把響應(yīng)式編程變成我們手中的利器圆恤。


響應(yīng)式的由來

我們先來聊一聊響應(yīng)式的由來突倍,對于它的由來,我們可能需要先從一段常見的代碼片段看起

int a=1;
int b=a+1;
System.out.print(“b=”+b)    //  b=2
a=10;
System.out.print(“b=”+b)    //  b=2

上面是一段很常見的代碼盆昙,簡單的賦值打印語句羽历,但是這種代碼有一個缺陷,那就是如果我們想表達(dá)的并不是一個賦值動作淡喜,而是b和a之間的關(guān)系,即無論a如何變化秕磷,b永遠(yuǎn)比a大1。那么可以想見炼团,我們就需要花額外的精力去構(gòu)建和維護(hù)一個b和a的關(guān)系澎嚣。

而響應(yīng)式編程的想法正是企圖用某種操作符幫助你構(gòu)建這種關(guān)系。
它的思想完全可以用下面的代碼片段來表達(dá):


int a=1;
int b <= a+1;   // <= 符號只是表示a和b之間關(guān)系的操作符
System.out.print(“b=”+b)    //  b=2
a=10;
System.out.print(“b=”+b)    //  b=11

這就是是響應(yīng)式的思想瘟芝,它希望有某種方式能夠構(gòu)建關(guān)系易桃,而不是執(zhí)行某種賦值命令。

至此你可能不禁要問锌俱,我們?yōu)槭裁葱枰獦?gòu)建關(guān)系的代碼而不是命令式的代碼呢晤郑?如果你翻一翻自己正在開發(fā)的APP,你就能看到的每一個交互的頁面其實內(nèi)部都包含了一系列的業(yè)務(wù)邏輯。而產(chǎn)品的每個需求造寝,其實也對應(yīng)了一系列的業(yè)務(wù)邏輯相互作用磕洪。總之,我們的開發(fā)就是在構(gòu)建一系列的業(yè)務(wù)邏輯之間的關(guān)系扔枫。你說我們是不是需要構(gòu)建關(guān)系的代碼蝶棋?

說回響應(yīng)式,前期由于真實的編程環(huán)境中并沒有構(gòu)建關(guān)系的操作符叫榕,主流的編程語言并不支持這種構(gòu)建關(guān)系的方式,所以一開始響應(yīng)式主要停留在想的層面姊舵,直到出現(xiàn)了Rx和一些其他支持這種思想的框架,才真正把響應(yīng)式編程引入到了實際的代碼開發(fā)中晰绎。

Rx是響應(yīng)式拓展,即支持響應(yīng)式編程的一種拓展,為響應(yīng)式在不同語言中的實現(xiàn)提供指導(dǎo)思想


什么是響應(yīng)式編程

說完了了響應(yīng)式的由來括丁,我們就可以談?wù)勈裁词琼憫?yīng)式編程了荞下。

響應(yīng)式編程是一種通過異步和數(shù)據(jù)流來構(gòu)建事務(wù)關(guān)系的編程模型。這里每個詞都很重要史飞,“事務(wù)的關(guān)系”是響應(yīng)式編程的核心理念尖昏,“數(shù)據(jù)流”和“異步”是實現(xiàn)這個核心理念的關(guān)鍵。為了幫助大家理解這個概念构资,我們不妨以APP初始化業(yè)務(wù)為例來拆解一下這幾個詞抽诉。

這是一個比較理想化的APP初始化邏輯,完成SDK初始化吐绵,數(shù)據(jù)庫初始化迹淌,登陸,之后跳轉(zhuǎn)主界面

事務(wù)的關(guān)系

  • 事務(wù)

    • 是一個十分寬泛的概念己单,它可以是一個變量唉窃,一個對象,一段代碼纹笼,一段業(yè)務(wù)邏輯.....但實際上我們往往把事務(wù)理解成一段業(yè)務(wù)邏輯(下文你均可以將事務(wù)替換為業(yè)務(wù)邏輯來理解)纹份,比如上圖中,事務(wù)就是指APP初始化中的四個業(yè)務(wù)邏輯廷痘。
  • 事務(wù)的關(guān)系

    • 這種關(guān)系不是類的依賴關(guān)系矮嫉,而是業(yè)務(wù)之間實際的關(guān)系。比如APP初始化中牍疏,SDK初始化蠢笋,數(shù)據(jù)庫初始化,登陸接口鳞陨,他們共同被跳轉(zhuǎn)頁面業(yè)務(wù)所依賴昨寞。但是他們?nèi)齻€本身并沒有關(guān)聯(lián)瞻惋。這也只是業(yè)務(wù)之間較為簡單的關(guān)系,實際上援岩,根據(jù)我們的需求App端會產(chǎn)生出許多業(yè)務(wù)之間錯綜復(fù)雜的關(guān)系歼狼。

數(shù)據(jù)流

關(guān)于Rx的數(shù)據(jù)流有很多說法,比如“Everything is a stream”,“Thinking with stream”等等享怀。雖然我明白這只是想強(qiáng)調(diào)流的重要性羽峰,可是這些話折射出來的編程思路其實是很虛無縹緲的,只會讓開發(fā)者對于Rx編程更加迷惑添瓷。

實際上梅屉,數(shù)據(jù)流只是事務(wù)之間溝通的橋梁。

比如在APP初始化中鳞贷,SDK初始化坯汤,數(shù)據(jù)庫初始化,登陸接口這些業(yè)務(wù)完成之后才會去安排頁面跳轉(zhuǎn)的操作搀愧,那么這些上游的業(yè)務(wù)在自己工作完成之后惰聂,就需要通知下游,通知下游的方式有很多種咱筛,其中最棒的的方式就是通過數(shù)據(jù)(事件)流搓幌。每一個業(yè)務(wù)完成后,都會有一條數(shù)據(jù)(一個事件)流向下游迅箩,下游的業(yè)務(wù)收到這條數(shù)據(jù)(這個事件)溉愁,才會開始自己的工作。

但是沙热,只有數(shù)據(jù)流是不能完全正確的構(gòu)建出事務(wù)之間的關(guān)系的。我們依然需要異步編程罢缸。

異步

異步編程本身是有很多優(yōu)點的篙贸,比如挖掘多核心CPU的能力,提高效率枫疆,降低延遲和阻塞等等爵川。但實際上,異步編程也給我們構(gòu)建事務(wù)的關(guān)系提供了幫助息楔。

在APP初始化中寝贡,我們能發(fā)現(xiàn)SDK初始化,數(shù)據(jù)庫初始化值依,登陸接口這三個業(yè)務(wù)本身相互獨立圃泡,應(yīng)當(dāng)在不同的線程環(huán)境中執(zhí)行,以保證他們不會相互阻塞愿险。而假如沒有異步編程颇蜡,我們可能只能在一個線程中順序調(diào)用這三個相對耗時較多的業(yè)務(wù),最終再去做頁面跳轉(zhuǎn),這樣做不僅沒有忠實反映業(yè)務(wù)本來的關(guān)系风秤,而且會讓你的程序“反應(yīng)”更慢

小結(jié)

總的來說鳖目,異步和數(shù)據(jù)流都是為了正確的構(gòu)建事務(wù)的關(guān)系而存在的。只不過缤弦,異步是為了區(qū)分出無關(guān)的事務(wù)领迈,而數(shù)據(jù)流(事件流)是為了聯(lián)系起有關(guān)的事務(wù)

APP初始化應(yīng)該怎么寫

許多使用Rx編程的同學(xué)可能會使用這種方式來完成APP的初始化碍沐。

Observable.just(context)
            .map((context)->{login(getUserId(context))})
            .map((context)->{initSDK(context)})
            .map((context)->{initDatabase(context)})
            .subscribeOn(Schedulers.newThread())
            .subscribe((context)->{startActivity()})

其實狸捅,這種寫法并不是響應(yīng)式的,本質(zhì)上還是創(chuàng)建一個子線程抢韭,然后順序調(diào)用代碼最后跳轉(zhuǎn)頁面薪贫。這種代碼依然沒有忠實反映業(yè)務(wù)之間的關(guān)系。

在我心目中刻恭,響應(yīng)式的代碼應(yīng)該是這樣的


Observable obserInitSDK=Observable.create((context)->{initSDK(context)}).subscribeOn(Schedulers.newThread())

Observable obserInitDB=Observable.create((context)->{initDatabase(context)}).subscribeOn(Schedulers.newThread())

Observable obserLogin=Observable.create((context)->{login(getUserId(context))})
                              .map((isLogin)->{returnContext()})
                            .subscribeOn(Schedulers.newThread())
                            
Observable observable = Observable.merge(obserInitSDK,obserInitDB,obserLogin)

observable.subscribe(()->{startActivity()})

大家應(yīng)該能很明顯看到兩段代碼的區(qū)別瞧省,第二段代碼完全遵照了業(yè)務(wù)之間客觀存在的關(guān)系,可以說代碼和業(yè)務(wù)關(guān)系是完全對應(yīng)的鳍贾。

那么這帶來了什么好處呢鞍匾?當(dāng)initSDK,initDB,Login都是耗時較長的操作時骑科,遵照業(yè)務(wù)關(guān)系編寫響應(yīng)式代碼可以極大的提高程序的執(zhí)行效率橡淑,降低阻塞。

理論上講咆爽,遵照業(yè)務(wù)關(guān)系運行的代碼在執(zhí)行效率上是最優(yōu)的梁棠。

為什么引入響應(yīng)式編程

對響應(yīng)式編程有了一些了解之后,我知道馬上會由很多人跳出來說斗埂,不使用這些響應(yīng)式編程我們還不是一樣開發(fā)APP符糊?

在這里我希望你理解一點,當(dāng)我們用老辦法開發(fā)APP的時候呛凶,其實做了很多妥協(xié)男娄,比如上面的APP初始化業(yè)務(wù),三個無關(guān)耗時操作為了方便漾稀,我們往往就放在一個線程環(huán)境中去執(zhí)行模闲,從而犧牲了程序運行的效率。而且實際開發(fā)中崭捍,這種類似的業(yè)務(wù)邏輯還有很多尸折,甚至更加復(fù)雜。假如不引入響應(yīng)式的思路殷蛇,不使用Rx的編程模型翁授,我們面對這么些復(fù)雜的業(yè)務(wù)關(guān)系真的會很糟心拣播。假如你做一些妥協(xié),那就會犧牲程序的效率收擦,假如你千辛萬苦構(gòu)建出業(yè)務(wù)關(guān)系贮配,最終寫出來的代碼也一定很復(fù)雜難以維護(hù)。所以塞赂,響應(yīng)式編程其實是一種更友好更高效的開發(fā)方式泪勒。

根據(jù)個人經(jīng)驗來看,響應(yīng)式編程至少有如下好處:

  • 在業(yè)務(wù)層面實現(xiàn)代碼邏輯分離宴猾,方便后期維護(hù)和拓展
  • 極大提高程序響應(yīng)速度圆存,充分發(fā)掘CPU的能力
  • 幫助開發(fā)者提高代碼的抽象能力和充分理解業(yè)務(wù)邏輯
  • Rx豐富的操作符會幫助我們極大的簡化代碼邏輯

一個復(fù)雜一些的例子

接下來,我就以我們團(tuán)隊目前的一款產(chǎn)品的頁面為例仇哆,詳細(xì)點介紹運用響應(yīng)式編程的正確姿勢沦辙。

首先,UI和產(chǎn)品溝通后讹剔,可能會給我們這樣的設(shè)計圖(加上一些尺寸的標(biāo)注)油讯。但是我們并不需要急忙編碼,我們首先要做的是區(qū)分其中相對獨立的模塊延欠。

上圖我做了一點簡單的標(biāo)注陌兑。把這個頁面的業(yè)務(wù)邏輯簡單的分為四個相互獨立的模塊,分別是視頻模塊由捎,在線人數(shù)模塊兔综,禮物模塊,消息模塊狞玛。他們相互獨立软驰,互不影響。接下來心肪,我們再去分析每個模塊內(nèi)部的業(yè)務(wù)并構(gòu)建起業(yè)務(wù)之間的關(guān)系锭亏。大致如下:

構(gòu)建了業(yè)務(wù)之間的關(guān)系圖,其實我們的工作已經(jīng)完成了一半了蒙畴,接下來就是用代碼實現(xiàn)這個關(guān)系圖贰镣。在這里呜象,我就以其中一小段業(yè)務(wù)關(guān)系來編寫代碼給大家示范膳凝。

    Observable obserDownload=Observable.just(url)
                                        .map((url)->{getZipFileFromRemote(url)});
    Observable obserLocal=Observable.just(url)
                                        .map((url)->{getZipFileFromLocal(url)});
    Observable obserGift=Observable.concat(obserLocal,obserDownload)
                                        .takeUnitl((file)->{file!=null});
    obserGift.subscribeOn(Schedulers.io()).flatMap((file)->{readBitmapsFromZipFile(file)})
                                        .subscribe((bitmap)->{showBitmap(bitmap)})


以上是我手寫的偽代碼,可能細(xì)節(jié)上有些問題恭陡,但大體思路就是這樣蹬音。

有人可能會說,那是因為你運用操作符比較熟練才能這么寫休玩。其實操作符都是我查的著淆,我記不住那么多操作符劫狠,所以基本上我都是先理清楚業(yè)務(wù)之間的關(guān)系,需要和并邏輯的時候永部,就去去查合并類的操作符独泞,需要條件判斷來分流的邏輯時去找條件判斷類的操作符√β瘢基本上都能滿足需求懦砂。你瞧,寫代碼就是這么簡單组橄,后續(xù)即使需要增加需求荞膘,代碼修改起來也很清晰,因為無關(guān)的業(yè)務(wù)已經(jīng)被你分離好了玉工。

所以羽资,趕緊在你的項目中引入響應(yīng)式編程吧!

勘誤

暫無

后記

前一段時間換了工作遵班,在目前的團(tuán)隊里除了實現(xiàn)日常需求屠升,也會負(fù)責(zé)項目重構(gòu)這一塊,我會更多的使用響應(yīng)式的方式重構(gòu)項目费奸,同時也會盡力在團(tuán)隊內(nèi)部推動響應(yīng)式編程的使用弥激,一旦有了新的體會,我也會在第一時間和大家分享愿阐。

由于這篇文章講的是響應(yīng)式編程微服,因此更多的使用的Rx這個名稱,而不是RxJava,因為RxJava只是響應(yīng)式編程在Java語言中的實現(xiàn)缨历。不過里面的偽代碼都是使用RxJava來編寫的以蕴,希望大家能夠理解。

假如你對RxJava不是十分了解的話辛孵,歡迎大家去看我之前寫的Rxjava系列文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丛肮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子魄缚,更是在濱河造成了極大的恐慌宝与,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冶匹,死亡現(xiàn)場離奇詭異习劫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)嚼隘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門诽里,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人飞蛹,你說我怎么就攤上這事谤狡【难郏” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵墓懂,是天一觀的道長焰宣。 經(jīng)常有香客問我,道長捕仔,這世上最難降的妖魔是什么宛徊? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮逻澳,結(jié)果婚禮上闸天,老公的妹妹穿的比我還像新娘。我一直安慰自己斜做,他們只是感情好苞氮,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瓤逼,像睡著了一般笼吟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上霸旗,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天贷帮,我揣著相機(jī)與錄音,去河邊找鬼诱告。 笑死撵枢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的精居。 我是一名探鬼主播锄禽,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼靴姿!你這毒婦竟也來了沃但?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤佛吓,失蹤者是張志新(化名)和其女友劉穎宵晚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體维雇,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡淤刃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谆沃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钝凶。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡仪芒,死狀恐怖唁影,靈堂內(nèi)的尸體忽然破棺而出耕陷,到底是詐尸還是另有隱情,我是刑警寧澤据沈,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布哟沫,位于F島的核電站,受9級特大地震影響锌介,放射性物質(zhì)發(fā)生泄漏嗜诀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一孔祸、第九天 我趴在偏房一處隱蔽的房頂上張望隆敢。 院中可真熱鬧,春花似錦崔慧、人聲如沸拂蝎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽温自。三九已至,卻和暖如春皇钞,著一層夾襖步出監(jiān)牢的瞬間悼泌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工夹界, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留馆里,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓可柿,卻偏偏與公主長得像也拜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子趾痘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容