松花皮蛋的黑板報
  • 分享在京東工作的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

掃一掃
關注公眾號

學習分享:DDD領域驅動設計指導微服務實踐

博客首頁文章列表 松花皮蛋me 2019-06-20 22:04

一、復雜性和規模增長的解決之道

解決復雜和大規模軟件的武器可以被粗略地歸為三類:抽象、分治和知識

1、分治

把問題空間分割為規模更小且易于處理的若干子問題。分割后的問題需要足夠小,以便一個人單槍匹馬就能夠解決他們;其次,必須考慮如何將分割后的各個部分裝配為整體。分割得越合理越易于理解,在裝配成整體時,所需跟蹤的細節也就越少。即更容易設計各部分的協作方式。評判什么是分治得好,即高內聚低耦合。以火車為例,每個車廂都要符合承重要求,行李車廂承重能力要高于其他車廂,車廂間的連接要牢固且易于拆解,有足夠的靈活性方便轉彎。另外大件行李應該單獨存放,避免占用普通車廂的空間,餐車應該在整個列車的中間,方便用餐

2、抽象

使用抽象能夠精簡問題空間,而且問題越小越容易理解。舉個例子,從北京到上海出差,可以先理解為使用交通工具前往,但不需要一開始就想清楚到底是高鐵還是飛機,以及乘坐他們需要注意什么

3、知識

通過知識手段抽象出限界上下文以及如何去分治

二、DDD概覽

DDD全稱為“Domain Driven Design”,意思是領域驅動設計,基于業務知識設計系統,代碼反映業務,它將真實業務概念和業務規則轉換為軟件系統中的概念和規則,在一個個有界上下文中發揮其作用,完成用戶要求的功能,從而降低或隱藏業務復雜性,使系統有更好的擴展性,以擁抱變化。說白了你那套領域邏輯到底有沒有效?邏輯上自洽為真,沒有邏輯矛盾,是不是現實中也是為真?需要在有界上下文中驗證

舉個例子:假如有父親和兒子這兩個表,生成的 POJO 應該是

public class Father{…}
public class Son{
  //son 表里有 fatherId 作為 Father 表 id 外鍵 
 private String fatherId;
 public String getFatherId(){
   return fatherId;
 }
 ……
}

這時候兒子犯了點什么錯,老爸非常不爽地扇了兒子一個耳光,老爸手疼,兒子臉疼。Manager通常這么做

public class SomeManager{
 public void fatherSlapSon(Father father, Son son){
  // 假設 painOnHand, painOnFace 都是數據庫字段
  father.setPainOnHand();
  son.setPainOnFace();
 }
}

這里,manager充當了上帝的角色,扇個耳光都得他老人家幫忙。但是現實世界應該是教訓兒子是自己的事情,并不需要別人幫忙,上帝也不行

public class Father{
 public void slapSon(Son son){
   this.setPainOnHand();
   son.setPainOnFace();
 }
}   

三、關注點

1、微服務關注點

  • 1)、運行時進程間通信,能夠容錯和故障隔離
  • 2)、去中心化管理數據和冶理
  • 3)、服務可以獨立的開發、測試、構建、部署
  • 4)、高內聚低耦合,職責單一

2、DDD關注點

  • 1)、關注業務領域,建立邊界并構建通用語言,高效溝通
  • 2)、對業務進行抽象,和業務專家一起建模
  • 3)、盡可能維持代碼和業務的低表示差異

一個供電系統中,合同部門關心的是電價,用電跟蹤部門關心的是電量,到了結算部門則是關心購電量,他們只關心自己所轄邊界內的重心,用不同方言在講話,沒有形成統一語言。實際上購電量=電價+電量+用戶時間段+用電規則,購電量才是業務模型中的核心方法,它應該是模型中的通用語言。有了統一語言后,可以減少將業務架構映射到系統架構時的層層翻譯,避免組件劃分過程中的邊界錯位,增強系統對業務的響應速度

從上面你可以發現DDD強調的是邏輯劃分,微服務強調的是隔離部署,系統架構的邏輯劃分可以細于部署單元的物理隔離,較好的架構應該是演進式,避免過早微服務化帶來的麻煩

四、DDD設計實踐

1、按業務劃分限界上下文

從業務能力的角度識別核心域、支撐域、通用域并去除二義性,比如電商業務中訂單就是核心域,訂單服務產生的其他業務則是支撐域,而通知中心、短信服務則是通用域

2、消除隱式數據依賴

一般情況下訂單表會使用user_id作為用戶標識,但如果系統將來要對接企業,我們可以新增一個訂單服務但是開發\維護成本過高,可以新增一個企業員工記錄但是員工離職交接維護成本過高,可以新增一個虛擬員工賬號但是無意間耦合了,正確做法是新增一個企業用戶上下文,然后使用”用戶ID加用戶類型”標識購買者,消除隱式數據依賴

《架構整潔之道》這本書中說到,任何形式的共享數據行為都會導致強耦合,如果給服務之間傳遞的數據記錄中增加一個新字段,那么每個需要操作這個字段的服務都必須要做出相應的變更,服務間必須對這條數據的解讀達成一致,那么他們是間接彼此耦合的

3、明確定義依賴方向

這里不得不提到整潔架構(又名洋蔥架構),整潔架構最主要原則是依賴原則,它定義了各層的依賴關系,越往里,依賴越低,代碼級別越高。外圓代碼依賴只能指向內圓,內圓不知道外圓的任何事情。一般來說,外圓的聲明(包括方法、類、變量)不能被內圓引用。同樣的,外圓使用的數據格式也不能被內圓使用

4、下游的自我保護

對于下流來說,需要根據自己的領域模型創建一個單獨的防腐層,該層作為上游系統的代理向你的系統提供功能,它在你自己的模型和他方模型間進行概念對象和其行為進行翻譯轉換,比如當數據實體格式不符合系統要求時,只需要在防腐層中添加對應的轉換器即可,領域模型可保持不變

六、DDD編碼的意義

讓代碼體現業務,保持二者的低表示差異,難點在于對聚合根的實現

在DDD模式中將對象分為值對象和實體。實體對象是有生命周期的,可以唯一標識的(不是數據庫中的ID),此對象只能屬于某個業務。而值對象是沒有生命周期的,比如訂單領域上下文中,訂單是實體、訂單項是實體、訂單狀態是值對象。原來我們系統劃分單位通常是模塊,但是粒度不夠細,所以需要對實體和值對象等進行關聯設計后,進行聚合的劃分和聚合根的確定,比如訂單和訂單項、訂單和訂單狀態有關聯,他們整體作為一個聚合,通常聚合中其他實體需要依賴聚合根,很顯然訂單應該是聚合根(生命周期最長,其他聚合項離開它沒有任何意義)。DDD模式中對一個聚合中實體的訪問或操作,必須通過這個聚合的聚合根開始,主要的目的是數據的最終一致性。另外聚合根之間不能建立關系,只能通過ID引用

比如

class Book {
  private @Id Long id;
  private Set<AutherRef> authors = new HashSet<>();

  public void addAuther(Author author) {
    authers.add(createAuthorRef(author));
  }

  private AuthorRef createAuthorRef(Author author) {
    AuthorRef authorRef = new AuthorRef();
    authorRef.author = author.id;
    return authorRef;
  }
}

七、總結

傳統開發模式是確定需求后定義表結構,以數據庫表作為系統導向,但是DDD建議應該先著眼于業務本身,減少對數據庫表結構的依賴,避免業務發生變化,把我們打得措手不及,它把你的思考起點從技術的角度拉到了業務上。不過在進行DDD設計時需要注意劃分邊界,注意定義邊界間的關系,注意概念不要穿透邊界

最后你會發現通篇都在談論的“邊界”劃分,我們知道微服務落地的難點之一就是如何正確折分,如果拆分后的服務出現互相調用或者需要高成本解決各個服務間的數據一致性,將得不償失,所以正確的打開方式是在進行微服務拆分前應該先充分了解領域驅動設計

注:文章內容來自最近的網上閱讀,如果讀者也想了解DDD,建議不要陷入DDD名詞中,推薦的書是《領域驅動設計精粹》,其他領域驅動的書你不要買,不然你肯定會把它們當做桌墊的

推薦閱讀

    閱讀 661 次
    黑龙江6+1开奖结果查询