從架構師的角度帶你把 “響應式編程” 給一次性搞明白,果然絕絕子
響應式編程詳解
響應式編程是一種基於異步數據流驅動、響應式、使用聲明式範式的編程模型,需要遵循一定的響應式編程開發規範,並且有具體的類庫實現。響應式編程基於數據流而不是控制流進行業務邏輯的推進。
響應式編程與設計模式
在面向對象編程語言中,響應式編程通常以觀察者模式呈現。將響應式流模式和迭代器模式比較,其主要區別是,迭代器基於 “拉” 模式,而響應式流基於 “推” 模式。
在命令編程範式中,開發者掌握控制流,使用迭代器遍歷 “數據”,使用 hasNext()函數判斷數據是否遍歷完成,使用 next()函數訪問下一個元素。在響應式編程模式中,使用觀察者模式,數據由消息發佈者(Publisher)發佈並通知訂閱者(Subscriber),而這種觀察者模式本身在基於事件監聽機制的響應式系統架構中被廣泛使用。Java 早期的 Swing 界面設計也是基於視圖事件觸發業務響應的系統工作模式。所以,從設計模式的角度講,響應式編程並不是新鮮事物,只是響應式編程將監聽的對象擴展到了更大範圍:靜態或者動態的 Stream 數據流,如下圖所示。
響應式編程還借鑑了 Reactor 設計模式,我們通常會在高性能 NIO 網絡通信框架中見到 Reactor 設計模式的身影,用來實現 I/O 多路複用。其基本思想是將所有要處理的 I/O 事件註冊到一箇中心 I/O 多路複用器上,同時主線程阻塞在多路複用器上,通過輪詢或者邊緣觸發的方式來處理網絡 I/O 事件。當有新的 I/O 事件到來或準備就緒時,多路複用器返回並將事件分發到對應的處理器中。Reactor 設計模式和響應式編程類似,它們都不主動調用某個請求的 API,而是通過註冊對應接口,實現事件觸發執行,如下圖所示。
響應式編程與響應式架構
響應式編程很容易和響應式架構混爲一談。前面我們介紹了響應式宣言中的構建軟件架構原則,把符合這些原則的系統稱爲響應式系統。如果說響應式系統與響應式編程之間具有什麼關係,那就是響應式系統的架構風格是響應式的,而響應式編程是實現這個架構風格的最佳實踐。從宏觀角度看,響應式系統由各種不同組件相互操作、調用組成,共同響應用戶請求。響應式系統涉及通信協議、I/O 模型、網絡傳輸、數據存儲等多方面因素,保障系統在響應力、擴展性、容錯、靈活性各方面表現出 “實時”“低延遲”“輕量”“健壯” 的系統特性。而響應式編程可能是這個大的系統架構下的一部分。另外,響應式系統一般是消息驅動的,而響應式編程是事件驅動的。
消息驅動與事件驅動
響應式宣言指出了兩者的區別:“消息驅動” 中消息數據被送往明確的目的地址,有固定導向;“事件驅動” 是事件向達到某個給定狀態的組件發出的信號,沒有固定導向,只有被觀察的數據。
-
在一個消息驅動系統中,可尋址的接收者等待消息的到來然後響應消息,否則保持休眠狀態,消息驅動系統專注於可尋址的接收者。響應式系統更加關注分佈式系統的通信和協作以達到解耦、異步的特性,滿足系統的彈性和容錯性,所以響應式系統更傾向於使用消息驅動模式。
-
在一個事件驅動系統中,通知的監聽者被綁定到消息源上。這樣當消息被髮出時,它就會被調用,所以,響應式編程更傾向於事件驅動。
響應式編程與函數式編程
響 應 式 編 程 同 時 容 易 和 函 數 式 編 程 混 淆 。函 數 式 編 程(Functional Reactive Programming,FRP)在二十年前就被 ConalElliott 精確地定義了。在函數式編程中,函數是第一類(firstclass)公民,函數式編程由 “行爲” 和“事件”組成。事件是基於時間的離散序列,而行爲是不可變的,是隨着時間連續變化的數據。函數式編程與響應式編程相比,它更偏重於底層編碼的實現細節。
從 Java 8 開始,Lambda 表達式的引入爲 Java 添加了函數式編程的特性,函數式編程提供了閉包的強大功能。Java 中的 Lambda 表達式通常使用(argument)->(body)語法書寫,如下所示:
下面是一些典型的 Lambda 表達式及其函數式接口:
-
Consumer c=(int x)->{System.out.println(x)};
-
BiConsumer<Integer, String>b=(Integer x, String y)->System.out.println(x+":"+y);
-
Predicatep=(String s)->{s==null};
在 Java 8 中新增加了 @FunctionalInterface 接口,用於指明該接口類型是根據 Java 語言規範定義的函數式接口。Java 8 還聲明瞭一些 Lambda 表達式可以使用的函數式接口。下面是匿名類和使用函數式編程方式的對比示例。
首先,使用 @FunctionalInterface 定義一個函數式編程接口。
然後,分別使用內部類和 Lambda 表達式兩種方式執行業務邏輯。
可以看到,在函數式編程中,Lambda 表達式允許將一個箭頭函數作爲參數進行傳遞,這樣的語法表達更加簡潔,而本質上由編譯器推斷並幫助實現轉換包裝爲常規代碼。因此,可以用更少的代碼來實現相同的功能。而響應式編程的重點是基於 “事件流” 的異步編程範式,響應式編程通過函數編程方式簡化面嚮對象語言語法的臃腫。響應式編程解決問題的流程是:將一個大的問題拆分爲許多獨立的小的步驟,而這些小的步驟都可以異步非阻塞地執行;當這些小的子任務執行完,它們會組成一個完整的工作流,並且這個工作流的輸入輸出都是非綁定的。實現響應式編程的關鍵就是 “非阻塞”,執行線程不會因爲競爭一個共享資源而陷入阻塞等待,空耗資源,並且最大化地利用物理資源。
響應式編程與命令式編程模式
響應式編程是一種聲明式的編程模型,與之相對應的就是命令模式(線程控制流)的編程模型。大家對命令式編程模式比較熟悉,下面是一段常見的基於命令式編程模式的代碼:
上述代碼是通過變量的賦值並通過加法計算響應數據之間的對應算數關係結果。但是,這個代碼有一個潛在的問題,當我們給這兩個變量重新賦值時,第二次的 Sum 值卻沒有變化,與我們的期望不符,原因是缺少了執行相加的命令指令。
響應式編程的目的是通過 “不可變操作符” 固定這種數據,構建數據之間的關係,並正確輸出結果,不會因爲操作命令的遺忘和缺失導致結果的偏差,造成對應關係和結果錯誤,下面我們看一下如何使用響應式編程方式來固化這種模式。
下面使用 Java 9 的 Flow API 實現兩個數的相加功能,按照相同思路,當傳入的變量不同時,輸出的 Sum 值也會隨着變化,我們把這種對應關係構建爲一個聲明公式,代碼實現如下:
從結果看,響應式編程模式的兩次 Sum 值和輸入的數值一致,能夠達到預期效果。從這個例子中,我們已經初步接觸到了響應式編程中數據源也就是事件發佈者(Publisher),還有就是事件的監聽回調函數集合——消費者(Subscriber)。消費者會根據 next、error、complet 觸發函數對應關係的執行,以及數據的操作符操作,由於消費者的不可變性,可以根據原生的數據結構生成新的數據結構。相比命令式編程,響應式編程使用操作符表述了一個通用業務執行邏輯,一般可以組合達到預期效果,一般的操作符還包含 map、filter、reduce 等函數,這裏就不再贅述了。
編程範式
“普通的工程師堆砌代碼,優秀的工程師優化代碼,卓越的工程師簡化代碼”。
如何寫出優雅整潔的代碼,不僅是一門學問,也是軟件工程的重要一環。在上一節中,我們簡單介紹了響應式編程的編程範式,本節我們進一步從開發者的視角、系統的性能、滿足用戶需求等方面討論不同編程範式的使用場景和特性優勢。
編程範式,又稱爲編程模型,泛指軟件編程過程中使用的編程風格,一般不同的編程範式具有不同的語法特性和差異。目前軟件開發技術中常用的典型編程範式有以下幾種。
-
命令式編程。
-
面向對象編程。
-
聲明式編程。
-
函數式編程。
因爲每一個編程範式都有很長的發展歷史,在編程語言支持上有不同的標準、組織和語法規範等,本節的目的是希望通過對這些編程範式的介紹,可以幫助我們更好地理解響應式編程範式。
命令式編程
命令式編程是非常傳統的軟件編程方式,命令式編程由不同的邏輯執行步驟組成,通過一步步指令的執行達到業務邏輯的推進,這種方式也稱爲過程式編程。命令式編程的執行過程非常符合計算機的執行步驟。C 語言是命令式編程的典型代表,它更關注的是機器域底層的內存、指令計算、輸入輸出。在 C 語言中,我們經常看到大段的過程式指令、各種 if/else/for 等控制語句、表達式、數據變量的操作、賦值等指令,這種純指令開發方式要求開發者對計算機的底層工作原理有非常深刻的理解,而且一個指令出現偏差往往會產生不可預知的錯誤。同時,命令式編程模式的運維也是難度非常高的。
面向對象編程
面向對象編程可以說是編程領域的一個分水嶺,開啓了高級程序語言在軟件開發上的統治階段。面向對象編程從問題域出發,將封裝、繼承、多態的語言特性映射到我們的現實世界。在面向對象編程裏,業務問題被抽象成類、接口模板,數據和行爲被統一封裝在對象內部,作爲程序的基本組成單元。面向對象編程範式在提升軟件重用性、靈活性和擴展性上比過程式編程更進一步,C++、Java 作爲面向對象編程語言的代表,屏蔽了機器底層的內存管理和機器域的管理細節。而面向對象編程雖然有較高的開發效率,但是降低了代碼的運行效率,這也限制了面向對象編程在性能要求苛刻場景下的應用。
聲明式編程
聲明式編程受當前 “約定優於配置” 理念的影響,在軟件編程開發領域中被大量應用。聲明式編程範式的好處是可以通過聲明的方式實現業務邏輯,不需要陷入底層具體的業務邏輯實現細節。聲明式編程範式關注的焦點不是採用什麼算法或者邏輯來解決問題,而是描述、聲明解決的問題是什麼。當你的代碼匹配預先設定好規則,業務邏輯就會被自動觸發執行。
很多標記性語言,如 HTML、XML、XSLT,就遵循聲明式編程範式,而 Spring Boot 基於註解方式的編程模型也是聲明式編程的一個代表。
Spring 框架依賴 AOP 和 IoC 編程思想降低了開發者對底層邏輯業務細節的瞭解程度。例如在 Spring Boot 中,通過 @Transactional 註解可以聲明一個方法具備事務性的操作,當異常發生時,事務會自動回滾,保證業務邏輯的正常和數據一致性。發生在 @Transactional 註解背後的實現細節,開發者可以不去關心。
函數式編程
在函數式編程範式中,函數無疑是一等公民,函數式編程最具魅力或者最重要的特性就是不可變性。它的不可變性表現在函數式編程表達式的執行結果,只取決於傳入函數的參數序列,不受數據狀態變化的影響。
函數式編程中的 Lambda 在 Java 8 中被引入,可以看成是兩個類型之間的關係:一個輸入類型和一個輸出類型。Lambda 演算就是給 Lambda 表達式一個輸入類型的值,它就可以得到一個輸出類型的值。
這個計算過程也是函數式代碼對映射的描述,因爲函數式代碼的抽象程度非常高,所以也意味着函數式代碼有更好的複用性。
函數式編程和命令式編程相比,更加關注消息或者數據的傳遞,而不像命令式編程,關注的是指令控制流。共享數據的狀態在多線程環境下會存在資源競爭的情況,往往我們需要把額外的精力投入到衝突地解決、數據狀態的維護中。而函數的不可變性保證了數據在傳遞處理過程中不會被篡改,也不需要依賴外部的鎖資源或者狀態來維護併發。所以函數式編程在多核處理器中具有天然的併發性,可以最大化地利用物理資源實現並行處理功能。
目前,在 JVM 體系中,已經出現了越來越多函數式編程範式的語言,例如 Scala、Groovy、Clojure 等。在當前計算機多核、數據優先、高性能的訴求下,函數式編程具有更廣闊的發展前景和未來。然而有利總會有弊,函數式編程的語法相比面向對象編程更晦澀,在大規模工程化的協調配合中,還是需要我們去權衡利弊。因爲無論哪種語言範式,本質上都是工具,最終目的都是爲業務服務。
來源:
https://www.toutiao.com/article/7138687931224441382/?log_from=1fe0ae5860d06_1662512946570
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/eJ9J1cFMzNNGHpdyeLzgEQ