什么是響應(yīng)式編程?
本文來(lái)源:https://blog.csdn.net/get_set/article/details/79455258作者:?享學(xué)IT本文已授作者轉(zhuǎn)載權(quán)限
1.1 什么是響應(yīng)式編程?
在開始討論響應(yīng)式編程(Reactive Programming)之前,先來(lái)看一個(gè)我們經(jīng)常使用的一款堪稱“響應(yīng)式典范”的強(qiáng)大的生產(chǎn)力工具——電子表格。
舉個(gè)簡(jiǎn)單的例子,某電商網(wǎng)站正在搞促銷活動(dòng),任何單品都可以參加“滿199減40”的活動(dòng),而且“滿500包郵”。吃貨小明有選擇障礙(當(dāng)然主要原因還是一個(gè)字:窮),他有個(gè)習(xí)慣,就是先在Excel上根據(jù)預(yù)算算好自己要買的東西:
img相信大家都用過(guò)Excel中的公式,這是一個(gè)統(tǒng)計(jì)購(gòu)物車商品和訂單應(yīng)付金額的表格,其中涉及到一些公式:
img上圖中藍(lán)色的線是公式的引用關(guān)系,從中可以看出,“商品金額”是通過(guò)“單價(jià)x數(shù)量”得到的,“滿199減40”會(huì)判斷該商品金額是否滿199并根據(jù)情況減掉40,右側(cè)“訂單總金額”是“滿199減40”這一列的和,“郵費(fèi)”會(huì)根據(jù)訂單總金額計(jì)算,“最終應(yīng)付款”就是訂單總金額加上郵費(fèi)。
1.1.1 變化傳遞(propagation of change)
為什么說(shuō)電子表格軟件是“響應(yīng)式典范”呢,因?yàn)椤皢蝺r(jià)”和“數(shù)量”的任何變動(dòng),都會(huì)被引用(“監(jiān)聽”)它的單元格實(shí)時(shí)更新計(jì)算結(jié)果,如果還有圖表或數(shù)據(jù)透視圖引用了這塊數(shù)據(jù),那么也會(huì)相應(yīng)變化,做到了實(shí)時(shí)響應(yīng)。變化的時(shí)候甚至還有動(dòng)畫效果,用戶體驗(yàn)一級(jí)棒!
這是響應(yīng)式的核心特點(diǎn)之一:變化傳遞(propagation of change)。一個(gè)單元格變化之后,會(huì)像多米諾骨牌一樣,導(dǎo)致直接和間接引用它的其他單元格均發(fā)生相應(yīng)變化。
title看到這里,你可能會(huì)說(shuō),“切~ 不就是算付款金額嗎,購(gòu)物網(wǎng)站上都有這個(gè)最基礎(chǔ)不過(guò)的功能啊~”,這就“響應(yīng)式”啦?但凡一個(gè)與用戶交互的系統(tǒng)都得“響應(yīng)”用戶交互啊~
但是在響應(yīng)式編程中,基于“變化傳遞”的特點(diǎn),觸發(fā)響應(yīng)的主體發(fā)生了變化。假設(shè)購(gòu)物車管理和訂單付款是兩個(gè)不同的模塊,或者至少是兩個(gè)不同的類——Cart和Invoice。也許我們的代碼是這樣的:
Product.java(假設(shè)商品有兩個(gè)屬性name和price,簡(jiǎn)單起見,price就不用BigDecimal類型了)
public?class?Product?{
????private?String?name;
????private?double?price;
????//?構(gòu)造方法、getters、setters
}
Cart模塊中:
import?com.example.Invoice;?//?2
public?class?Cart?{
????...
????public?boolean?addProduct(Product?product,?int?quantity)?{
????????...
????????double?figure?=?product.getPrice()?*?quantity;
????????invoice.update(figure);?//?1
????????...
????}
????...
}
是由
Cart的對(duì)象去調(diào)用Invoice對(duì)象的更新訂單金額的方法;Cart的代碼中需要importInvoice。

而我們?cè)儆^察這個(gè)Excel,發(fā)現(xiàn)“訂單總金額”的計(jì)算公式不僅位于自己的單元格中,而且這個(gè)公式能主動(dòng)監(jiān)聽和響應(yīng)購(gòu)物車數(shù)據(jù)的變化事件。對(duì)于購(gòu)物車來(lái)說(shuō),它沒(méi)有對(duì)訂單付款方面的任何公式引用。感覺(jué)就像這樣:
假設(shè)數(shù)據(jù)流有操作的商品product和變化個(gè)數(shù)quantity兩個(gè)屬性:
public?class?CartEvent?{
????private?Product?product;
????private?int?quantity;
????//?構(gòu)造方法、getters、setters
}
Invoice模塊中:
import?com.example.Cart?//?2
public?class?Invoice?{
????...
????public?Invoice(Cart?cart)?{
????????...
????????this.listenOn(cart);????//?1
????????...
????}
????//?回調(diào)方法
????public?void?onCartChange(CartEvent?event)?{
????????...
????}
????...
}
是由
Invoice的對(duì)象在初始化的時(shí)候就聲明了對(duì)Cart對(duì)象的監(jiān)聽,從而一旦Cart對(duì)象有響應(yīng)的事件(比如添加商品)發(fā)生的時(shí)候,Invoice就會(huì)響應(yīng);Invoice的代碼中importCart。
title做過(guò)Java桌面開發(fā)的朋友可能會(huì)想到Java swing中的各種監(jiān)聽器,比如MouseListener能夠監(jiān)聽鼠標(biāo)的操作,并實(shí)時(shí)做出響應(yīng)。所以C/S的客戶端總是比B/S的Web界面更具有響應(yīng)性嘛。
所以,這里我們說(shuō)的是一種生產(chǎn)者只負(fù)責(zé)生成并發(fā)出數(shù)據(jù)/事件,消費(fèi)者來(lái)監(jiān)聽并負(fù)責(zé)定義如何處理數(shù)據(jù)/事件的變化傳遞方式。
那么,Cart對(duì)象如何在發(fā)生變化的時(shí)候“發(fā)出”數(shù)據(jù)或事件呢?
1.1.2 數(shù)據(jù)流(data stream)
這些數(shù)據(jù)/事件在響應(yīng)式編程里會(huì)以數(shù)據(jù)流的形式發(fā)出。
我們?cè)儆^察一下購(gòu)物車,這里有若干商品,小明每次往購(gòu)物車?yán)锾砑踊蛞瞥环N商品,或調(diào)整商品的購(gòu)買數(shù)量,這種事件都會(huì)像過(guò)電一樣流過(guò)這由公式串起來(lái)的多米諾骨牌一次。這一次一次的操作事件連起來(lái)就是一串?dāng)?shù)據(jù)流(data stream),如果我們能夠及時(shí)對(duì)數(shù)據(jù)流的每一個(gè)事件做出響應(yīng),會(huì)有效提高系統(tǒng)的響應(yīng)水平。這是響應(yīng)式的另一個(gè)核心特點(diǎn):基于數(shù)據(jù)流(data stream)。
如下圖是小明選購(gòu)商品的過(guò)程,為了既不超預(yù)算,又能省郵費(fèi),有時(shí)加有時(shí)減:
數(shù)據(jù)流這一次一次的操作就構(gòu)成了一串?dāng)?shù)據(jù)流。Invoice模塊中的代碼可能是這樣:
????public?Invoice(Cart?cart)?{
????????...
????????this.listenOn(cart.eventStream());??//?1
????????...
????}
其中,
cart.eventStream()是要監(jiān)聽的購(gòu)物車的操作事件數(shù)據(jù)流,listenOn方法能夠?qū)?shù)據(jù)流中到來(lái)的元素依次進(jìn)行處理。
1.1.3 聲明式(declarative)
我們?cè)俚?code style="font-size:inherit;color:rgb(248,35,117);">listenOn方法去看一下:
Invoice模塊中,上邊的一串公式被組裝成如下的偽代碼:
????public?void?listenOn(DataStream?cartEventStream) ?{???//?1
????????double?sum?=?0;
????????double?total?=?cartEventStream
????????????//?分別計(jì)算商品金額
????????????.map(cartEvent?->?cartEvent.getProduct().getPrice()?*?cartEvent.getQuantity())??//?2
????????????//?計(jì)算滿減后的商品金額
????????????.map(v?->?(v?>?199)???(v?-?40)?:?v)
????????????//?將金額的變化累加到sum
????????????.map(v?->?{sum?+=?v;?return?sum;})
????????????//?根據(jù)sum判斷是否免郵,得到最終總付款金額
????????????.map(sum?->?(sum?>?500)???sum?:?(sum?+?50));
????????...
cartEventStream是數(shù)據(jù)流,DataStream是某種數(shù)據(jù)流類型,可以暫時(shí)想象成類似在Java 8版本增加的對(duì)數(shù)據(jù)流進(jìn)行處理的Stream API(下節(jié)會(huì)說(shuō)到為啥不用Java Stream)。map方法用于對(duì)數(shù)據(jù)流中的元素進(jìn)行映射,比如第一個(gè)將cartEvent中的商品價(jià)格和數(shù)量拿到,然后算出本次操作的金額;第二個(gè)判斷是否能享受“滿199減40”的活動(dòng)。
這里的偽代碼用到了lambda,它非常適用于數(shù)據(jù)流的處理。沒(méi)有接觸過(guò)lambda的話沒(méi)有關(guān)系,我們后續(xù)會(huì)再聊到它。
這是一種“聲明式(declarative)”的編程范式。通過(guò)四個(gè)串起來(lái)的map調(diào)用,我們先聲明好了對(duì)于數(shù)據(jù)流“將會(huì)”進(jìn)行什么樣的處理,當(dāng)有數(shù)據(jù)流過(guò)來(lái)時(shí),就會(huì)按照聲明好的處理流程逐個(gè)進(jìn)行處理。
比如對(duì)于第一個(gè)map操作:
title聲明式編程范式的威力在于以不變應(yīng)萬(wàn)變。無(wú)論到來(lái)的元素是什么,計(jì)算邏輯是不變的,從而形成了一種對(duì)計(jì)算邏輯的“綁定”。
再舉個(gè)簡(jiǎn)單的例子方便理解:
a?=?1;
b?=?a?+?1;
a?=?2;
這個(gè)時(shí)候,b是多少呢?在Java以及多數(shù)語(yǔ)言中,b的結(jié)果是2,第二次對(duì)a的賦值并不會(huì)影響b的值。
假設(shè)Java引入了一種新的賦值方式:=,表示一種對(duì)a的綁定關(guān)系,如
a?=?1;
b?:=?a?+?1;
a?=?2;
由于b保存的不是某次計(jì)算的值,而是針對(duì)a的一種綁定關(guān)系,所以b能夠隨時(shí)根據(jù)a的值的變化而變化,這時(shí)候b==3,我們就可以說(shuō):=是一種聲明式賦值方式。而普通的=是一種命令式賦值方式。事實(shí)上,我們絕大多數(shù)的開發(fā)都是命令式的,如果需要用命令式編程表達(dá)類似上邊的這種綁定關(guān)系,在每次a發(fā)生變化并需要拿到b的時(shí)候都得執(zhí)行b = a + 1來(lái)更新b的值。
如此想來(lái),“綁定美元政策”不也是一種聲明式的范式嗎~
總結(jié)來(lái)說(shuō),命令式是面向過(guò)程的,聲明式是面向結(jié)構(gòu)的。
不過(guò)命令式和聲明式本身并無(wú)高低之分,只是聲明式比較適合基于流的處理方式。這是響應(yīng)式的第三個(gè)核心特點(diǎn):聲明式(declarative)。結(jié)合“變化傳遞”的特點(diǎn),聲明式能夠讓基于數(shù)據(jù)流的開發(fā)更加友好。
1.1.4 總結(jié)
總結(jié)起來(lái),響應(yīng)式編程(reactive programming)是一種基于數(shù)據(jù)流(data stream)和變化傳遞(propagation of change)的聲明式(declarative)的編程范式。
響應(yīng)式編程的“變化傳遞”就相當(dāng)于果汁流水線的管道;在入口放進(jìn)橙子,出來(lái)的就是橙汁;放西瓜,出來(lái)的就是西瓜汁,橙子和西瓜、以及機(jī)器中的果肉果汁以及殘?jiān)龋际橇鲃?dòng)的“數(shù)據(jù)流”;管道的圖紙是用“聲明式”的語(yǔ)言表示的。
這種編程范式如何讓W(xué)eb應(yīng)用更加“reactive”呢?
我們?cè)O(shè)想這樣一種場(chǎng)景,我們從底層數(shù)據(jù)庫(kù)驅(qū)動(dòng),經(jīng)過(guò)持久層、服務(wù)層、MVC層中的model,到用戶的前端界面的元素,全部都采用聲明式的編程范式,從而搭建一條能夠傳遞變化的管道,這樣我們只要更新一下數(shù)據(jù)庫(kù)中的數(shù)據(jù),用戶的界面上就相應(yīng)的發(fā)生變化,豈不美哉?尤其重要的是,一處發(fā)生變化,我們不需要各種命令式的調(diào)用來(lái)傳遞這種變化,而是由搭建好的“流水線”自動(dòng)傳遞。
這種場(chǎng)景用在哪呢?比如一個(gè)日志監(jiān)控系統(tǒng),我們的前端頁(yè)面將不再需要通過(guò)“命令式”的輪詢的方式不斷向服務(wù)器請(qǐng)求數(shù)據(jù)然后進(jìn)行更新,而是在建立好通道之后,數(shù)據(jù)流從系統(tǒng)源源不斷流向頁(yè)面,從而展現(xiàn)實(shí)時(shí)的指標(biāo)變化曲線;再比如一個(gè)社交平臺(tái),朋友的動(dòng)態(tài)、點(diǎn)贊和留言不是手動(dòng)刷出來(lái)的,而是當(dāng)后臺(tái)數(shù)據(jù)變化的時(shí)候自動(dòng)體現(xiàn)到界面上的。
兩年嘔心瀝血的文章:「面試題」「基礎(chǔ)」「進(jìn)階」這里全都有!
長(zhǎng)按掃碼可關(guān)注獲取?
在看和分享對(duì)我非常重要!
