1. 程式人生 > >(1)什麽是響應式編程——響應式Spring的道法術器

(1)什麽是響應式編程——響應式Spring的道法術器

響應式編程

本系列文章索引:《響應式Spring的道法術器》。

1 響應式編程之道

1.1 什麽是響應式編程?

在開始討論響應式編程(Reactive Programming)之前,先來看一個我們經常使用的一款堪稱“響應式典範”的強大的生產力工具——電子表格。

舉個簡單的例子,某電商網站正在搞促銷活動,任何單品都可以參加“滿199減40”的活動,而且“滿500包郵”。吃貨小明有選擇障礙(當然主要原因還是一個字:窮),他有個習慣,就是先在Excel上根據預算算好自己要買的東西:

技術分享圖片

相信大家都用過Excel中的公式,這是一個統計購物車商品和訂單應付金額的表格,其中涉及到一些公式:

技術分享圖片

上圖中藍色的線是公式的引用關系,從中可以看出,“商品金額”是通過“單價x數量”得到的,“滿199減40”會判斷該商品金額是否滿199並根據情況減掉40,右側“訂單總金額”是“滿199減40”這一列的和,“郵費”會根據訂單總金額計算,“最終應付款”就是訂單總金額加上郵費。

1.1.1 變化傳遞(propagation of change)

為什麽說電子表格軟件是“響應式典範”呢,因為“單價”和“數量”的任何變動,都會被引用(“監聽”)它的單元格實時更新計算結果,如果還有圖表或數據透視圖引用了這塊數據,那麽也會相應變化,做到了實時響應。變化的時候甚至還有動畫效果,用戶體驗一級棒!

這是響應式的核心特點之一:變化傳遞(propagation of change)。一個單元格變化之後,會像多米諾骨牌一樣,導致直接和間接引用它的其他單元格均發生相應變化。

技術分享圖片

看到這裏,你可能會說,“切~ 不就是算付款金額嗎,購物網站上都有這個最基礎不過的功能啊~”,這就“響應式”啦?但凡一個與用戶交互的系統都得“響應”用戶交互啊~

但是在響應式編程中,基於“變化傳遞”的特點,觸發響應的主體發生了變化。假設購物車管理和訂單付款是兩個不同的模塊,或者至少是兩個不同的類——CartInvoice。也許我們的代碼是這樣的:

Product.java(假設商品有兩個屬性nameprice,簡單起見,price就不用BigDecimal類型了)

    public class Product {
        private String name;
        private double price;
        // 構造方法、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
        ...
    }
    ...
}
  1. 是由Cart的對象去調用Invoice對象的更新訂單金額的方法;
  2. Cart的代碼中需要import Invoice

技術分享圖片

而我們再觀察這個Excel,發現“訂單總金額”的計算公式不僅位於自己的單元格中,而且這個公式能主動監聽和響應購物車數據的變化事件。對於購物車來說,它沒有對訂單付款方面的任何公式引用。感覺就像這樣:

假設數據流有操作的商品product和變化個數quantity兩個屬性:

public class CartEvent {
    private Product product;
    private int quantity;
    // 構造方法、getters、setters
}

Invoice模塊中:

import com.example.Cart // 2

public class Invoice {
    ...
    public Invoice(Cart cart) {
        ...
        this.listenOn(cart);    // 1
        ...
    }
    // 回調方法
    public void onCartChange(CartEvent event) {
        ...
    }
    ...
}
  1. 是由Invoice的對象在初始化的時候就聲明了對Cart對象的監聽,從而一旦Cart對象有響應的事件(比如添加商品)發生的時候,Invoice就會響應;
  2. Invoice的代碼中import Cart

技術分享圖片

做過Java桌面開發的朋友可能會想到Java swing中的各種監聽器,比如MouseListener能夠監聽鼠標的操作,並實時做出響應。所以C/S的客戶端總是比B/S的Web界面更具有響應性嘛。

所以,這裏我們說的是一種生產者只負責生成並發出數據/事件,消費者來監聽並負責定義如何處理數據/事件的變化傳遞方式

那麽,Cart對象如何在發生變化的時候“發出”數據或事件呢?

1.1.2 數據流(data stream)

這些數據/事件在響應式編程裏會以數據流的形式發出。

我們再觀察一下購物車,這裏有若幹商品,小明每次往購物車裏添加或移除一種商品,或調整商品的購買數量,這種事件都會像過電一樣流過這由公式串起來的多米諾骨牌一次。這一次一次的操作事件連起來就是一串數據流(data stream),如果我們能夠及時對數據流的每一個事件做出響應,會有效提高系統的響應水平。這是響應式的另一個核心特點:基於數據流(data stream)

如下圖是小明選購商品的過程,為了既不超預算,又能省郵費,有時加有時減:

技術分享圖片

這一次一次的操作就構成了一串數據流。Invoice模塊中的代碼可能是這樣:

    public Invoice(Cart cart) {
        ...
        this.listenOn(cart.eventStream());  // 1
        ...
    }
  1. 其中,cart.eventStream()是要監聽的購物車的操作事件數據流,listenOn方法能夠對數據流中到來的元素依次進行處理。

1.1.3 聲明式(declarative)

我們再到listenOn方法去看一下:

Invoice模塊中,上邊的一串公式被組裝成如下的偽代碼:

    public void listenOn(DataStream<CartEvent> cartEventStream) {   // 1
        double sum = 0;
        double total = cartEventStream
            // 分別計算商品金額
            .map(cartEvent -> cartEvent.getProduct().getPrice() * cartEvent.getQuantity())  // 2
            // 計算滿減後的商品金額
            .map(v -> (v > 199) ? (v - 40) : v)
            // 將金額的變化累加到sum
            .map(v -> {sum += v; return sum;})
            // 根據sum判斷是否免郵,得到最終總付款金額
            .map(sum -> (sum > 500) ? sum : (sum + 50));
        ...
  1. cartEventStream是數據流,DataStream是某種數據流類型,可以暫時想象成類似在Java 8版本增加的對數據流進行處理的Stream API(下節會說到為啥不用Java Stream)。

  2. map方法用於對數據流中的元素進行映射,比如第一個將cartEvent中的商品價格和數量拿到,然後算出本次操作的金額;第二個判斷是否能享受“滿199減40”的活動。

這裏的偽代碼用到了lambda,它非常適用於數據流的處理。沒有接觸過lambda的話沒有關系,我們後續會再聊到它。

這是一種“聲明式(declarative)”的編程範式。通過四個串起來的map調用,我們先聲明好了對於數據流“將會”進行什麽樣的處理,當有數據流過來時,就會按照聲明好的處理流程逐個進行處理。

比如對於第一個map操作:

技術分享圖片

聲明式編程範式的威力在於以不變應萬變。無論到來的元素是什麽,計算邏輯是不變的,從而形成了一種對計算邏輯的“綁定”。

再舉個簡單的例子方便理解:

a = 1;
b = a + 1;
a = 2;

這個時候,b是多少呢?在Java以及多數語言中,b的結果是2,第二次對a的賦值並不會影響b的值。

假設Java引入了一種新的賦值方式:=,表示一種對a的綁定關系,如

a = 1;
b := a + 1;
a = 2;

由於b保存的不是某次計算的值,而是針對a的一種綁定關系,所以b能夠隨時根據a的值的變化而變化,這時候b==3,我們就可以說:=是一種聲明式賦值方式。而普通的=是一種命令式賦值方式。事實上,我們絕大多數的開發都是命令式的,如果需要用命令式編程表達類似上邊的這種綁定關系,在每次a發生變化並需要拿到b的時候都得執行b = a + 1來更新b的值。

如此想來,“綁定美元政策”不也是一種聲明式的範式嗎~

總結來說,命令式是面向過程的,聲明式是面向結構的

不過命令式和聲明式本身並無高低之分,只是聲明式比較適合基於流的處理方式。這是響應式的第三個核心特點:聲明式(declarative)。結合“變化傳遞”的特點,聲明式能夠讓基於數據流的開發更加友好。

1.1.4 總結

總結起來,響應式編程(reactive programming)是一種基於數據流(data stream)和變化傳遞(propagation of change)的聲明式(declarative)的編程範式。

響應式編程的“變化傳遞”就相當於果汁流水線的管道;在入口放進橙子,出來的就是橙汁;放西瓜,出來的就是西瓜汁,橙子和西瓜、以及機器中的果肉果汁以及殘渣等,都是流動的“數據流”;管道的圖紙是用“聲明式”的語言表示的。

這種編程範式如何讓Web應用更加“reactive”呢?

我們設想這樣一種場景,我們從底層數據庫驅動,經過持久層、服務層、MVC層中的model,到用戶的前端界面的元素,全部都采用聲明式的編程範式,從而搭建一條能夠傳遞變化的管道,這樣我們只要更新一下數據庫中的數據,用戶的界面上就相應的發生變化,豈不美哉?尤其重要的是,一處發生變化,我們不需要各種命令式的調用來傳遞這種變化,而是由搭建好的“流水線”自動傳遞。

這種場景用在哪呢?比如一個日誌監控系統,我們的前端頁面將不再需要通過“命令式”的輪詢的方式不斷向服務器請求數據然後進行更新,而是在建立好通道之後,數據流從系統源源不斷流向頁面,從而展現實時的指標變化曲線;再比如一個社交平臺,朋友的動態、點贊和留言不是手動刷出來的,而是當後臺數據變化的時候自動體現到界面上的。

具體如何來實現呢,請看下一節關於響應式流的介紹。

(1)什麽是響應式編程——響應式Spring的道法術器