使用函數式語言實踐DDD

長期以來我都在實踐OOP,進而經過OOP來實現DDD,特別是如何經過面向對象的技巧來創建一個領域模型。OO的一些特性在創建領域模型時顯得恰如其分,可否掌握OO的技巧,對建立領域模型有着相當重要的做用。
這篇文章爲你們介紹一種常見的函數式架構,特別是如何經過函數式語言來實現DDD,進而利用函數式組合的特性,建立函數pipeline。
軟件架構是圍繞着領域模型而作的若干設計,若是按照c4模型的定義,軟件架構由下面四個級別的架構組成的:html

  • "System context"是最高層的架構,表明着整個系統
  • "Container"是組成"System context"的單元,一般用來表示可部署的單元,例如一個"API service", 一個web應用程序等
  • "Component"是組成"Container"的基本單元,一般指組若干抽象組件,是一個"Container"裏面的骨架,也是本文要重點介紹的架構
  • "Code"具體到了代碼級別,一般指實現某個"Component"應該有哪幾個類組成

使用單體應用來承載多個限界上下文

領域驅動設計中有一半概念是在討論問題域,並非一上來就教你如何寫代碼,這說明理解一個問題域是複雜的,看清問題的本質是須要時間的。當你開始着手劃分限界上下文的時候,說明你已經對需求有了很好的瞭解。可是經驗告訴咱們,剛開始你的理解,每每都不是最終的需求,或者仍然須要屢次跟領域專家確認和交互,才能獲得最終的需求。
這個時候,若是你一上來就按照限界上下文劃分微服務,每每可能會步入Microservice Premium
要想軟件在一開始就能達到快速試錯的目的,一上來就作微服務, 會讓步子邁得有點大。微服務架構帶來了分佈式的複雜性,使得前期生產效率大大下降,另外還存在船大難掉頭的狀況,一旦設計出現返工,生產效率也會打折扣。固然,這不是絕對的,若是架構師已經在該行業深耕多年,對業務更是瞭如指掌,項目一開始就設計爲微服務也何嘗不可。
在項目初期,在需求還不是很是明確的時候,你徹底能夠建立一個單體應用,而後經過不一樣的模塊或程序集來隔離不一樣的界限上下文,經過不斷的試錯和快速反饋來調整你的解決方案。
一種比較嚴格的說法是,當你關閉其中一個微服務,若是整個應用程序都崩了,其實你設計的不是一個微服務架構,而是一個分佈式單體應用程序。git

代碼結構

在過去的若干年裏,我常用一種叫「Layer architecture"的軟件架構, 這種架構每每把代碼分紅若干層:web

  • 基礎設施層:一般用來負責跟第三方或者數據庫打交道,用來持久化數據或者API請求。
  • 領域層或者業務邏輯層:用來封裝業務邏輯
  • 應用程序層:一般是很薄的一層,用來協調領域層和基礎設施層
  • 展示層:用來展示UI或者輸出API結果
    這種架構方式是一個自上往下的輸入,最後從下往上輸出結果的工做流(圖1)。

    實際上,當我在使用這種方式組織代碼時,遇到最大的挑戰在於:這種分層方式,把同一個輸入到輸出的的若干部分,橫向的分散到了若干層中。當你須要修改某個API時,須要同時修改若干個層。另外這種組織代碼的方式,每每會讓OO走向混亂,一個名叫OrderApplicationService的類中放滿了各類跟Order相關的方法,一般對Order的操做有數十種之多,他們屬於OrderApplicationService嗎?若是屬於,任何一個跟Order相關操做的參數變化,都會引發這個類被改動,這種對類的頻繁修改合理嗎?
    函數式編程中,更傾向於縱向組織代碼(圖2),

    例如一個API操做,就是一個文件或者模塊,整個操做自上而下的流程被組織到同一個文件裏,這樣作的好處是,針對某個功能的修改,只關注與當前工做流相關的文件便可。

信任邊界

在問題域裏,各類業務之間的邊界是模糊的,限界上下文則是業務在解決方案上的映射,是人爲劃分的邊界。在邊界裏面的內容,是可信任和合法的,相反,界限外面的一切輸入,則是非法和不可信任的(圖3)。

這就要求咱們在限界上下文的邊界,引入驗證邏輯,從而阻止外部輸入,以及驗證對外部的輸出。
常見的驗證邏輯如:typescript

  • 輸入DTO,須要轉化爲領域模型,用於處理業務邏輯
  • 對輸入數據的合法性驗證,例如:用戶名不能爲空,郵件格式是否正確
  • 對輸出類型的安全性校驗,例如:防止在輸出數據裏包含用戶密碼等敏感信息
    驗證邏輯並非FP獨有的,不過FP中經常使用Applicative對數據進行驗證,從而收集多個用戶Error。關於Applicative, 之後會單獨寫文章介紹。
    一旦輸入數據突破信任邊界,在領域模型建模的過程當中,你不須要擔憂用戶名是不是空,郵件格式是否正確等問題。你應該專一於使用FP的代數數據類型進行領域建模,請參考我以前寫過一篇使用函數式語言來創建領域模型--類型組合
    對輸出的驗證則不太同樣,主要關心對輸出數據的安全性保護,防止將一些領域模型中的私有屬性輸出到外部世界。

經過狀態機來處理業務邏輯

縱然,經過FP的代數數據類型(Algebraic data type)可以快速完成領域建模,可是咱們知道,領域模型不是靜態的,它是由一些列事件組成的過程。而這種轉化過程,正是領域模型狀態發生變化的過程,即狀態機(圖4)。

領域模型狀態轉換的過程跟實現語言無關,一個設計精良的領域模型,就比如一個狀態機。例如在買機票的過程當中,填寫我的信息,填寫聯繫人,選座,買保險和付款的過程,就是訂單狀態發生變化的過程。再好比用戶註冊的過程,填寫基本信息,驗證郵箱,也是用戶信息狀態發生變化的過程。以OO爲例,咱們習慣於經過增長標誌位的方式,進行領域建模:數據庫

type User = {
  name: string
  password: string
  email: Email | null 
  isEmailVerified: boolean //當驗證完email後設置爲true
  canLogin: boolean //當email被驗證後方可login
}

業務邏輯的實現過程,就是填充用戶屬性和修改標誌位的過程。然而,這種方式實際上存在若干問題:編程

  • 有些屬性在業務前期是不須要的,例如canLogin, 只有驗證完email纔有效
  • 有些標誌位實際上不是單獨存在的,例如isEmailVerified就跟email是緊密相關的,而這個模型沒法反映出來這一信息
  • email被定義爲可空類型,致使使用該模型的地方不得不使用null檢查
    經過狀態機的機制,從新考慮用戶註冊過程:(圖5)

按照上面的狀態從新對用戶建模,獲得的模型以下:json

type UnVerifiedUser = {
  name: string
  password: string
}

type VerifiedEmailUser = {
  name: string
  password: string
  email: Email
}

type User =
  | UnVerifiedUser
  | VerifiedEmailUser

若是有更多的用戶狀態,你還能夠持續添加到User類型中。
這種經過"|"建立的User類型被稱爲在FP中被稱爲union類型,也叫product或sum類型, 在TypeScript被稱爲Discriminated union。這時候的User類型,能夠用來在領域模型中實現領域邏輯,一般這種union類型須要配合模式匹配來完成,例如修改密碼,登陸,修改郵件地址等邏輯,都是針對User類型作模式匹配的過程。關於模式匹配的用法,在此再也不細說。
這種經過狀態機的方式,實現業務邏輯時有下面幾個好處:數組

  • 業務模型在不一樣的狀態,提供不一樣的業務能力
  • 模式匹配會強制你處理每種狀態的行爲,避免遺漏一些邊邊角角的狀況
  • 相比於將全部狀態記錄在同一個模型中,狀態機能夠幫你梳理整個業務狀態的變化

保持純淨的領域模型

函數式編程的一個主要目標就是讓代碼有預測性,經過函數簽名理解函數的用途。爲了達到這個目的,函數式語言設計了若干特性,例如不可變的數據結構,還有各種Monad來避免反作用。在DDD實踐中,應該避免I/O相關的代碼出現Domain中。例如讀寫數據庫,調用第三方系統的API等相關代碼,須要把這類具備反作用的代碼推到Domain的外圍。若是須要作的更好,那就必須使用CQRS加Event Sourcing。我在以前一篇文章提到過這個觀點,不過部分讀者沒有理解其中的意思,我在這裏再作一些說明。首先,CQRS不只僅是爲了讀寫分離,從而提升讀寫性能。讀模型和寫模型(領域模型)的分離意味着職責也是分離的,從而在設計領域模型的時候,打消對查詢性能的考慮,有助於設計出純淨的領域模型。固然僅靠CQRS仍是不夠的,有些時候任然沒法徹底脫離數據庫的考慮,由於領域模型始終是要持久化在數據庫裏,你就要考慮數據庫相關的約束,例如主外鍵,如何建表,如何高效存儲一個列表等。而持久化一個Event則徹底擺脫了數據庫技術,由於一個Event就是一個json, 只有這樣才能設計出理想的領域模型。固然引入CQRS和ES在項目初期成本略高,再也不詳細描述。安全

經過Monad建立pipeline

以API爲例,一個完整的用戶請求就是一個Pipeline(圖6)。

假設每一步都是有若干個函數組成,咱們可以將他們組合到一塊兒嗎?答案是很難,主要緣由以下:數據結構

  • 每一步的若干個函數簽名很難保持一致,致使compose這樣的函數沒法正常工做
  • 部分I/O相關的函數多是異步的,領域模型中的代碼大可能是同步的,很難將他們組合在一塊兒
  • 在函數式編程中,一般不會經過try...catch的方式處理異常,一方面異常也是一種反作用,另外一方面,異常讓函數簽名再也不完整。如何把每一步的異常帶到最外面也成了問題
    而解決這一切的手段就是Monad, 簡而言之,Monad是一種抽象方式,可以將monadic風格的函數鏈接起來。什麼又是monadic? 簡單來講這是一種接收普通類型,返回某種lift類型(泛型)的函數。例如經過IO, Task, Either相關的Monad來解決此類問題。具體內容請關注本人的函數式系列博客。

小結

這篇文章總結了一些使用函數式語言實踐DDD的大體思路,也爲函數式架構提供了一些參考。因爲篇幅的緣由,並無介紹到DDD的方方面面,同時,一些實現細節則是點到爲止,例如如何使用Monad。整體來講,函數式語言的代數數據類型,以及函數式的一些思想,爲實踐領域驅動設計提供了其餘的選項。

相關文章
相關標籤/搜索