函式式DDD架構入門 - SCOTT WLASCHIN

banq發表於2020-02-28

幫助工程團隊將函式程式設計原理應用到高階設計和體系結構與架構的通俗易懂的思想和最佳實踐。

關於函數語言程式設計或FP的許多文章都專注於低階編碼實踐(例如避免副作用)和FP特定模式(例如可怕的monad)。但是,它們不涉及高階設計和體系結構。然而,FP原則可以大規模應用。實際上,從後端的無伺服器到前端的Redux / Elm風格的框架,許多流行的框架和架構樣式都源於函數語言程式設計。

如果使用得當,FP原理可以降低複雜性,同時提高應用程式的可測試性和可維護性。這是函式架構。

FP原理適用於軟體架構

函數語言程式設計的三個原理與軟體架構特別相關。首先是函式是獨立的值。也就是說,可以像對待其他獨立值一樣對待它們,例如整數和字串。可以將它們分配給變數,儲存在列表中,作為引數傳遞,作為結果返回等等。

在函式架構中,基本單元也是函式,但是我喜歡稱其為工作流workflow,工作流是函式的基本單元。它可以稱為一個功能特性、用例、場景、故事或任何您想呼叫的。就像函式一樣在編碼級別是“某個事物”,這些工作流是架構級別的“事物”,是架構的基本構建塊。

其次,組合是構建系統的主要方式。只需將一個輸出連線到另一個輸入即可構成兩個簡單功能。結果是可以用作更多合成的起點的另一個功能。

組合是一個非常重要的概念,函式式程式設計師擁有一套標準工具,例如monads,即使輸入和輸出不完全匹配也可以進行組合。

從體系結構和架構的角度來看,由較小的函式組成較大的函式,其結果最明顯是,函式系統看起來像帶有輸入和輸出的管道,而不是面向訊息的請求/響應模型。

函式式DDD架構入門 - SCOTT WLASCHIN

每個工作流程函式通常具有相同的結構:讀取資料、制定業務決策並根據需要轉換資料,最後,在另一端輸出任何新資料或事件。這些步驟中的每一個都可以依次視為較小的函式。分支和其他型別的複雜性可能會發揮作用,但是即使工作流程變得越來越大和越來越複雜,資料也始終會朝一個方向流動。

這種組合方法意味著我們僅結合了特定業務工作流所需的特定元件。不需要傳統的分層體系結構。當我們向系統中新增新功能時,每個新工作流程所需的函式都是獨立定義的,而不是分組為資料庫或服務層。

如果我們確實需要在不同的工作流中使用完全相同的函式,則可以將該函式一次定義為子函式,然後在需要它的工作流中將其重新用作共享步驟。這就是組合方法如此吸引人的原因:工作流作為獨立的單元進行設計和構建,僅包含其所需的功能,但是當需要時,我們仍然可以利用重用和元件化的所有好處。

最後,函式式程式設計師嘗試儘可能多地使用純函式。純函式是確定性的(給定的輸入始終會導致相同的輸出),並且沒有副作用(例如突變或I / O)。它們非常易於測試(確定性!),並且易於理解而無需深入研究其實現(無副作用!)。

與外界互動

您不需要鼓勵開發人員使用洋蔥架構,作為FP 方法的副作用它會自動發生。

當然,在某些時候,我們將需要進行I / O操作-讀寫檔案,訪問資料庫等等。函式式程式設計師試圖將這種不確定性儘可能地保持在管道的邊緣。某些語言(例如Elm和Haskell)對此非常嚴格,不允許有任何偏差,而其他語言則將其更多地視為準則而不是規則。

該函式模型與眾所周知的方法非常相似,例如洋蔥架構六邊形架構(也稱為埠和介面卡架構)以及函式核心,命令式外殼。在所有情況下,核心域(純業務邏輯)都與基礎架構隔離。基礎結構程式碼瞭解核心域,但並非相反。依賴關係是單向的,I / O保持在邊緣。

函式式DDD架構入門 - SCOTT WLASCHIN

僅將純程式碼用於業務邏輯意味著單元測試和整合測試之間存在明顯的區別。單元測試是針對核心領域的,它是確定性和快速的,而整合測試則是從頭到尾對工作流進行的。

函數語言程式設計的優點之一是,業務域與基礎結構的這種隔離是自然發生的。您不需要鼓勵開發人員使用洋蔥體系結構。作為FP 方法的副作用它會自動發生。

邊界和背景

作為軟體設計師和架構師,我們的下一個挑戰是決定如何將這些工作流或管道分組為邏輯單元。與往常一樣,這更多的是藝術而不是科學。

有許多準則可以提供幫助。低耦合和高內聚的經典原理不僅適用於函式程式碼,而且適用於物件導向的程式碼。或者,重述通用封閉原則:一起變化的程式碼應該一起生活。最近,領域驅動設計(DDD)社群非常重視元件邊界以及在何處繪製邊界。

函式式DDD架構入門 - SCOTT WLASCHIN

在DDD術語中,相關函式的分組稱為有界上下文,每個有界上下文在其自身許可權中都被視為一個微型域。它通常對應於特定業務功能的邏輯封裝。我們用“有界上下文”而不是像之前稱為“子系統”,是因為它使我們能夠專注於設計解決方案時最重要的事情:瞭解上下文和邊界。(如何定義這些有界上下文正是另一篇文章的主題。)

為什麼要上下文?因為每個上下文代表一些專業知識或能力。在上下文中,我們共享一種通用語言,並且設計是連貫一致的。但是,就像在現實世界中一樣,從上下文中提取資訊可能會造成混亂或無法使用。

太寬或太模糊的邊界根本就沒有邊界。

為什麼要有邊界?在現實世界中,領域可能具有模糊邊界。但是在軟體領域,我們希望減少子系統之間的耦合,以便它們可以獨立發展。邊界是確保子系統保持獨立的關鍵,可以使用標準軟體實踐(例如使用顯式API)和避免依賴項(例如共享程式碼)來維護子系統。在需求不斷變化的複雜專案中,我們必須毫不留情地維護有界上下文的“有界”部分。太寬或太模糊的邊界根本就沒有邊界。

自治是有界上下文的關鍵方面。具有自主權意味著有界上下文可以做出決策,而不必等待來自其他有界上下文的決策或資訊。也就是說,如果一個有界上下文不可用,則其他有界上下文可以繼續獨立執行,這是重要的分離。

自治也可以應用於開發過程。通常,一個有界上下文最好由一個團隊擁有。想想三腿比賽:綁在腿上的兩個跑步者比自由地獨立跑步的兩個跑步者慢得多。軟體元件也是如此。如果兩個團隊在相同的受限環境中做出貢獻,那麼他們可能最終會隨著設計的發展而朝著不同的方向拉動設計。

如果將有界上下文的概念應用於函式體系結構,我們最終會遇到許多小型的、集中的域,每個域都支援許多業務工作流。這些邊界的定義方式應使其內部的工作流具有自主性,並且能夠在不依賴其他系統的情況下完成其工作。

在某些情況下,長期執行的用例或場景需要多個工作流。在這種情況下,處於不同上下文中的工作流將需要使用事件和其他方法相互通訊。但是,重要的是將單個工作流保持在一個有界上下文中,並且不要嘗試實現方案在多種情況下“端到端”。允許工作流到達多個服務內部最終將導致我們很好解耦的體系結構演變成無法維護的依賴關係的糾結- “ 一個大麻煩” 。”

實體服務反模式

定義邊界可能沒有正確的方法,但是肯定有許多錯誤的方法。

定義邊界可能沒有一種正確的方法,但是肯定有許多錯誤的方法。分組功能的常見反模式是“ 實體服務”方法,其中圍繞實體而不是工作流構建服務。也就是說,有一個“訂單”服務,一個“產品”服務,等等。這通常是由於天真地將物件導向的設計直接轉移到面向工作流程的體系結構而導致的。此設計的主要問題是,單個業務工作流通常將需要所有這些服務進行協作。如果其中任何一個不可用,則整個工作流程將失敗。而且,如果工作流程需要發展,我們可能需要同時觸控和更新許多服務中的程式碼,從而破壞了“一起改變的程式碼應該一起生活”的規則。

此外,僅因為業務工作流程涉及實體,例如“訂單”並不意味著它與使用該實體的其他工作流程有任何共同點。例如,“支付訂單”工作流和“刪除訂單”工作流都涉及訂單,但是具有完全不同的業務邏輯。不需要他們都依賴於將不同函式集合在一起的“訂購”服務。當其他要求(例如安全性,可伸縮性等)開始發揮作用時,我們可能會發現必須以非常不同的方式來管理不同的工作流程。耦合它們只會引起痛苦!

事件

現在,我們的工作流函式已分組到有界上下文中,可以使用了。但是什麼觸發了這些業務工作流?是什麼導致員工,使用者或自動化流程啟動工作流?

是一個事件。也就是說,在外面的世界有新的變化-客戶在點選一個按鈕,郵件到達時,警報彈出。這是在一個商業活動的形式捕獲-例如,“下訂單”或“已收到電子郵件。” 在FP架構中,像這樣的業務事件會觸發工作流。

此外,使用這種方法,工作流的輸出也是一個事件:一個通知,通知所有下游工作流世界上發生了重要變化。特定於特定工作流程且未共享的更改(例如資料庫更新)不會作為事件從工作流程中發出。

這就是我們可以從這些較小的工作流程中組裝較大的流程的方式。每個工作流程都由一個事件觸發,並且該工作流程又會生成更多事件供下游流程使用。但是事件如何在工作流之間傳遞?這取決於專案的特定要求。如果所有工作流程都可以生活在同一個流程中,那麼它可以是一個簡單的記憶體佇列。但是,如果需要分別和獨立地部署工作流,則首選外部佇列,服務匯流排或Kafka風格的事件日誌。

請注意,在所有情況下,工作流都是非同步互動的。這使他們在時間和空間上保持獨立和分離。很少使用在工作流中直接呼叫另一個工作流的命令和控制方法。

如果您熟悉事件驅動的體系結構,則將很熟悉這種基於事件的方法。而且,確實,具有單獨管道的FP方法非常適合於這種體系結構樣式。

邏輯架構與物理架構

對事件觸發的單獨工作流的描述是邏輯檢視,而不是物理檢視。

到目前為止,設計目標已經圍繞業務需求進行了調整,但是我們還需要考慮技術需求。贊成使用工作流作為設計的單元的一個觀點是:它們可以在物理上部署在多種方式-例如作為微服務,獨立無伺服器函式,或單體架構的元件模組化,或甚至作為在基於代理的系統Erlang或Akka的風格。技術實現的選擇取決於開發團隊的規模和數量,安全性,可伸縮性需求等。

在某些情況下,邏輯工作流也可能在物理上分為單獨的部分。例如,工作流可以通過從前端開始同步API 呼叫,然後繼續在後端執行。

那麼,應該清楚的是,系統的邏輯和物理組成是不相同的,不應混為一談。但這並不意味著我們可以避免就架構的物​​理和實現方面做出決策。單體或無伺服器是在專案早期應考慮的重要決定。同樣,程式語言的選擇和資料庫的選擇也沒有反映在像這樣的邏輯模型中,但它們是至關重要的體系結構決策。

前端函式架構

隨著SPA的興起以及完全寫在前端的嚴肅應用程式,前端軟體體系結構變得越來越重要。隨著FP強調不變性、單向資料流和邊緣處的I / O已被證明對於降低複雜性很有價值,用於前端架構的函式性方法也變得越來越流行。

最常見的功能前端體系結構是Model-View-Update體系結構,也稱為Elm體系結構。在此設計中,應用程式包含一個不變的模型(代表應用程式狀態)和兩個關鍵功能:一個update功能是在發生來自瀏覽器的訊息或事件(例如單擊按鈕)時更新模型的view功能,以及一個呈現檢視該模型(通常只是HTML)。呈現的檢視可以將瀏覽器事件與域模型中定義的訊息相關聯,以便隨後在瀏覽器中單擊按鈕(例如,在瀏覽器中)將觸發域訊息,從而觸發更新功能,該函式最終將呈現新檢視,即再次傳遞給瀏覽器。以此類推。

函數語言程式設計原理的力量

從後端的微服務和無伺服器到前端的MVU,函數語言程式設計的知識對於理解許多現代架構樣式至關重要。

軟體體系結構的許多良好做法:凝聚力,去耦,I / O的隔離等等,從應用程式設計函式的原則自然會出現。例如,我們已經看到,在典型的函式設計中,每個工作流都是獨立構建的,僅包含其所需的功能(最大限度地提高了凝聚力),並且在各個層次上都強調了自治性,從單個函式到有界上下文(去耦) 。此外,確保可測試性和可維護性的一種確保方法是將業務邏輯保持在純確定性的函式中,並使用不可變的資料模型來迫使資料更改變得明確。

有許多方法可以進行軟體體系結構,並且沒有一種千篇一律的方法。但是,使用函數語言程式設計原理的潛在好處是多方面的,我鼓勵您進一步研究這些原理-甚至可以將其應用於下一個專案。祝好運!

本文基於 Scott Wlaschin撰寫的函式的強大的領域建模的第3章。

 

相關文章