Domain-Driven Design(領(lǐng)域驅(qū)動設(shè)計,或 DDD)
基礎(chǔ)概念
Domain Primitive
是一個在特定領(lǐng)域里,擁有精準定義的、可自我驗證的、擁有行為的 Value Object 公条。
1、DP 是一個傳統(tǒng)意義上的 Value Object迂曲,擁有 Immutable 的特性
2靶橱、DP 是一個完整的概念整體,擁有精準定義
3、DP 使用業(yè)務(wù)域中的原生語言
4关霸、DP 可以是業(yè)務(wù)域的最小組成部分传黄、也可以構(gòu)建復雜組合
分析維度:接口的清晰度(可閱讀性)、數(shù)據(jù)驗證和錯誤處理队寇、業(yè)務(wù)邏輯代碼的清晰度膘掰、和可測試性。
使用 Domain Primitive 的三原則
1佳遣、Make Implicit Concepts Explicit 將隱性的概念顯性化
2识埋、Make Implicit Context Explicit 將 隱性的 上下文 顯性化
3、Encapsulate Multi-Object Behavior 封裝 多對象 行為
什么情況下應(yīng)該用 Domain Primitive
1零渐、有格式限制的 String:比如 Name窒舟,PhoneNumber,OrderNumber诵盼,ZipCode惠豺, Address 等。
2风宁、有限制的 Integer:比如 OrderId(>0)洁墙,Percentage(0-100%),Quantity(>=0) 等戒财。
3热监、可枚舉的 int :比如 Status(一般不用 Enum 因為反序列化問題)。
4饮寞、Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有業(yè)務(wù)含義的孝扛,比如 T emperature、Money骂际、Amount、ExchangeRate冈欢、Rating 等歉铝。 5、復雜的數(shù)據(jù)結(jié)構(gòu):比如 Map<String, List<Integer>> 等凑耻,盡量能把 Map 的所有操作 包裝掉太示,僅暴露必要行為。
實戰(zhàn) - 老應(yīng)用重構(gòu)的流程
第一步 - 創(chuàng)建 Domain Primitive香浩,收集所有 DP 行為
第二步 - 替換數(shù)據(jù)校驗和無狀態(tài)邏輯
第三步 - 創(chuàng)建新接口
第四步 - 修改外部調(diào)用
在做架構(gòu)設(shè)計時类缤,一個好的架構(gòu)應(yīng)該需要實現(xiàn)以下幾個目標:
1、獨立于框架:架構(gòu)不應(yīng)該依賴某個外部的庫或框架邻吭,不應(yīng)該被框架的結(jié)構(gòu)所束縛餐弱。
2、獨立于 UI:前臺展示的樣式可能會隨時發(fā)生變化(今天可能是網(wǎng)頁、明天可能變成 console膏蚓、后天是獨立 app)瓢谢,但是底層架構(gòu)不應(yīng)該隨之而變化。
3驮瞧、獨立于底層數(shù)據(jù)源:無論今天你用 MySQL氓扛、Oracle 還是MongoDB、CouchDB论笔,甚至使用文件系統(tǒng)采郎,軟件架構(gòu)不應(yīng)該因為不同的底層數(shù)據(jù)儲存方式而產(chǎn)生巨大改變。
4狂魔、獨立于外部依賴:無論外部依賴如何變更蒜埋、升級,業(yè)務(wù)的核心邏輯不應(yīng)該隨之而大幅變 化毅臊。
5理茎、可測試:無論外部依賴了什么數(shù)據(jù)庫、硬件管嬉、UI 或者服務(wù)皂林,業(yè)務(wù)的邏輯應(yīng)該都能夠快 速被驗證正確性。
事務(wù)腳本類的代碼很難維護因為以下幾點:
問題 1-可維護性能差
可維護性 = 當依賴變化時蚯撩,有多少代碼需要隨之改變
1础倍、數(shù)據(jù)結(jié)構(gòu)的不穩(wěn)定性:AccountDO 類是一個純數(shù)據(jù)結(jié)構(gòu),映射了數(shù)據(jù)庫中的一個表胎挎。 這里的問題是數(shù)據(jù)庫的表結(jié)構(gòu)和設(shè)計是應(yīng)用的外部依賴沟启,長遠來看都有可能會改變,比 如數(shù)據(jù)庫要做 Sharding犹菇,或者換一個表設(shè)計德迹,或者改變字段名。
2揭芍、依賴庫的升級:AccountMapper 依賴 MyBatis 的實現(xiàn)胳搞,如果 MyBatis 未來升級版本, 可能會造成用法的不同(可以參考 iBatis升級到基于注解的MyBatis 的遷移成本)称杨。同 樣的肌毅,如果未來換一個 ORM 體系,遷移成本也是巨大的姑原。
3悬而、第三方服務(wù)依賴的不確定性:第三方服務(wù),比如Yahoo的匯率服務(wù)未來很有可能會有變化:輕則API簽名變化锭汛,重則服務(wù)不可用需要尋找其他可替代的服務(wù)笨奠。在這些情況下改造和遷移成本都是巨大的袭蝗。同時,外部依賴的兜底艰躺、限流呻袭、熔斷等方案都需要隨之改 變。
4腺兴、第三方服務(wù) API 的接口變化:YahooForexService.getExchangeRate 返回的結(jié)果是小數(shù) 點還是百分比左电?入?yún)⑹牵╯ource, target)還是(target, source)?誰能保證未來接口 不會改變页响?如果改變了篓足,核心的金額計算邏輯必須跟著改,否則會造成資損闰蚕。
5栈拖、中間件更換:今天我們用 Kafka 發(fā)消息,明天如果要上阿里云用 RocketMQ 該怎么辦没陡? 后天如果消息的序列化方式從 String 改為 Binary 該怎么辦涩哟?如果需要消息分片該怎么 改?
問題 2-可拓展性差
可擴展性 = 做新需求或改邏輯時盼玄,需要新增/修改多少代碼
1贴彼、數(shù)據(jù)來源被固定、數(shù)據(jù)格式不兼容
2埃儿、業(yè)務(wù)邏輯無法復用
3器仗、邏輯和數(shù)據(jù)存儲的相互依賴
問題 3-可測試性能差
可測試性 = 運行每個測試用例所花費的時間 * 每個需求所需要增加的測試用例數(shù)量
1、設(shè)施搭建困難:當代碼中強依賴了數(shù)據(jù)庫童番、第三方服務(wù)精钮、中間件等外部依賴之后,想要 完整跑通一個測試用例需要確保所有依賴都能跑起來剃斧,這個在項目早期是及其困難的轨香。 在項目后期也會由于各種系統(tǒng)的不穩(wěn)定性而導致測試無法通過。
2幼东、運行耗時長:大多數(shù)的外部依賴調(diào)用都是 I/O 密集型臂容,如跨網(wǎng)絡(luò)調(diào)用、磁盤調(diào)用等筋粗,而 這種 I/O 調(diào)用在測試時需要耗時很久策橘。另一個經(jīng)常依賴的是笨重的框架如 Spring炸渡,啟 動 Spring 容器通常需要很久娜亿。當一個測試用例需要花超過 10 秒鐘才能跑通時,絕大部 分開發(fā)都不會很頻繁的測試蚌堵。
3买决、耦合度高:假如一段腳本中有 A沛婴、B、C 三個子步驟督赤,而每個步驟有 N 個可能的狀態(tài)嘁灯,當多個子步驟耦合度高時,為了完整覆蓋所有用例躲舌,最多需要有 N * N * N 個測試用 例丑婿。當耦合的子步驟越多時,需要的測試用例呈指數(shù)級增長没卸。
防腐層(ACL)Anti-Corruption Layer
1羹奉、適配器:很多時候外部依賴的數(shù)據(jù)、接口和協(xié)議并不符合內(nèi)部規(guī)范约计,通過適配器模式诀拭, 可以將數(shù)據(jù)轉(zhuǎn)化邏輯封裝到 ACL 內(nèi)部,降低對業(yè)務(wù)代碼的侵入煤蚌。 2耕挨、緩存:對于頻繁調(diào)用且數(shù)據(jù)變更不頻繁的外部依賴,通過在ACL里嵌入緩存邏輯尉桩,能夠有效的降低對于外部依賴的請求壓力筒占。同時,很多時候緩存邏輯是寫在業(yè)務(wù)代碼里的魄健, 通過將緩存邏輯嵌入 ACL赋铝,能夠降低業(yè)務(wù)代碼的復雜度。
3沽瘦、兜底:如果外部依賴的穩(wěn)定性較差革骨,一個能夠有效提升我們系統(tǒng)穩(wěn)定性的策略是通過ACL起到兜底的作用,比如當外部依賴出問題后析恋,返回最近一次成功的緩存或業(yè)務(wù)兜 底數(shù)據(jù)良哲。這種兜底邏輯一般都比較復雜,如果散落在核心業(yè)務(wù)代碼中會很難維護助隧,通過 集中在 ACL 中筑凫,更加容易被測試和修改。
4并村、易于測試:類似于之前的 Repository巍实,ACL 的接口類能夠很容易的實現(xiàn) Mock 或 Stub,以便于單元測試哩牍。
5棚潦、功能開關(guān):有些時候我們希望能在某些場景下開放或關(guān)閉某個接口的功能,或者讓某個接口返回一個特定的值膝昆,我們可以在ACL配置功能開關(guān)來實現(xiàn)丸边,而不會對真實業(yè)務(wù)代 碼造成影響叠必。同時,使用功能開關(guān)也能讓我們?nèi)菀椎膶崿F(xiàn) Monkey 測試妹窖,而不需要真正物理性的關(guān)閉外部依賴纬朝。