1. 程式人生 > >領域驅動設計(DDD:Domain-Driven Design) 侵立刪

領域驅動設計(DDD:Domain-Driven Design) 侵立刪

轉自:https://www.jdon.com/ddd.html

 

領域驅動設計(DDD:Domain-Driven Design)

  Eric Evans的“Domain-Driven Design領域驅動設計”簡稱DDD,Evans DDD是一套綜合軟體系統分析和設計的面向物件建模方法,本站Jdon.com是國內公開最早討論DDD網站之一,可訂閱DDD專題。初學者學習DDD可從研究本站Jdon框架的DDD應用原始碼開始,戳這裡開始

  過去系統分析和系統設計都是分離的,正如我們國家“系統分析師” 和“系統設計師” 兩種職稱考試一樣,這樣割裂的結果導致,需求分析的結果無法直接進行設計程式設計,而能夠進行程式設計執行的程式碼卻扭曲需求,導致客戶執行軟體後才發現很多功能不是自己想要的,而且軟體不能快速跟隨需求變化。

  DDD則打破了這種隔閡,提出了領域模型概念,統一了分析和設計程式設計,使得軟體能夠更靈活快速跟隨需求變化。見下面DDD與傳統CRUD或過程指令碼或者面向資料表等在開發效率上比較:

ddd

  伺服器後端發展三個階段:

  1. UI+DataBase的兩層架構,這種面向資料庫的架構(上圖table module )沒有靈活性。
  2. UI+Service+DataBase的多層SOA架構,這種服務+表模型的架構易使服務變得囊腫,難於維護拓展,伸縮效能差,見這裡討論Spring Web 應用的最大敗筆.
  3. DDD+SOA的事件驅動的CQRS讀寫分離架構,應付複雜業務邏輯,以聚合模型替代資料表模型,以併發的事件驅動替代串聯的訊息驅動。真正實現以業務實體為核心的靈活拓展。

  DDD革命性在於:領域模型準確反映了業務語言,而傳統J2EE或Spring+Hibernate等事務性程式設計模型只關心資料,這些資料物件除了簡單setter/getter方法外,沒有任何業務方法,被比喻成失血模型,那麼領域模型這種帶有業務方法的充血模型到底好在哪裡?

  以比賽Match為案例,比賽有“開始”和“結束”等業務行為,但是傳統經典的方式是將“開始”和“結束”行為放在比賽的服務Service中,而不是放在比賽物件本身之中。我們不能因為用了計算機,用了資料庫,用了框架,業務模型反而被技術框架給綁架,就像人雖然是由母親生的,但是人的吃喝拉撒母親不能替代,更不能以母愛名義肢解人的正常職責行為,如果是這樣,這個人就是被母愛綁架了。

  提倡充血模型,實際就是讓過去被肢解被黑crack的業務模型迴歸正常,當然這也會被一些先入為主或被洗過腦的程式設計師看成反而不正常,這更是極大可悲之處。看到領域模型程式碼,就看到業務需求,沒有翻譯沒有轉換,保證軟體真正實現“拷貝不走樣”。

  DDD最大的好處是:接觸到需求第一步就是考慮領域模型,而不是將其切割成資料和行為,然後資料用資料庫實現,行為使用服務實現,最後造成需求的首肢分離。DDD讓你首先考慮的是業務語言,而不是資料。重點不同導致程式設計世界觀不同。

 

  DDD是解決複雜中大型軟體的一套行之有效方式,在國外已經成為主流。DDD認為很多原因造成軟體的複雜性,我們不可能避免這些複雜性,能做的是對複雜的問題進行控制。而一個好的領域模型是控制複雜問題的關鍵。領域模型的價值在於提供一種通用的語言,使得領域專家和軟體技術人員聯絡在一起,溝通無歧義。

  DDD在軟體生產流程中定位i如下圖,DDD落地實現離不開in-memory快取、 CQRS、 DCI、 EDAEvent Source幾大大相關領域。

cache

 

轉自:https://www.cnblogs.com/softidea/p/7257910.html

領域驅動的火爆程度不用我贅述,但是即便其如此得耳熟能詳,但大多數人對其的認識,還只是停留在知道它的縮寫是DDD,知道它是一種軟體思想,或者知道它和微服務有千絲萬縷的關係。Eric Evans對DDD的詮釋是那麼地惜字如金,而我所認識的領域驅動設計的專家又都是行業中的資深前輩,他們擅長於對軟體設計進行高屋建瓴的論述,如果沒有豐富的網際網路從業經驗,是不能從他們的分享中獲取太多的營養的,可以用曲高和寡來形容。1000個網際網路從業者,100個懂微服務,10個人懂領域驅動設計。

可能有很多和我一樣的讀者,在得知DDD如此火爆之後,嘗試去讀了開山之作《領域驅動設計——軟體核心複雜性應對之道》,翻看了幾張之後,晦澀的語句,不明所以的專業術語,加上翻譯導致的語句流暢性,可以說觀看體驗並不是很好,特別是對於開發經驗不是很多的讀者。我總結了一下,為何這本書難以理解: 
1. 沒有閱讀軟體設計叢書的習慣,更多人偏向於閱讀偏應用層面的書籍,“talk is cheap,show me the code”往往更符合大多數人的習慣。 
2. 沒有太多的開發經驗支撐。沒有踩過坑,就不會意識到設計的重要性,無法產生共情。 
3. 年代有些久遠,這本書寫於2004年,書中很多軟體設計的反例,在當時是非常流行的,但是在現在已經基本絕跡了。大師之所以為大師,是因為其能跨越時代的限制,預見未來的問題,這也是為什麼DDD在十幾年前就被提出,卻在微服務逐漸流行的現階段才被大家重視。

誠然如標題所示,本文是領域驅動設計的一個入門文章,或者更多的是一個個人理解的筆記,筆者也正在學習DDD的路上,可能會有很多的疏漏。如有理解有偏頗的地方,還望各位指摘。

認識領域驅動設計的意義

領域驅動設計並不會絕對地提高專案的開發效率。 

圖1:複雜性與開發週期關係

 

遵循領域驅動設計的規範使得專案初期的開發甚至不如不使用它來的快,原因有很多,程式設計師的素質,程式碼的規範,限界上下文的劃分…甚至需求修改後導致需要重新建模。但是遵循領域驅動設計的規範,在專案越來越複雜之後,可以不至於讓專案僵死。這也是為什麼很多系統不斷迭代著,最終就黃了。書名的副標題“軟體核心複雜性應對之道”正是闡釋了這一點。

 

模式: smart ui是個反模式

可能很多讀者還不知道smart ui是什麼,但是在這本書寫作期間,這種設計風格是非常流行的。在與一位領域驅動設計方面的資深專家的交談中,他如下感慨到軟體發展的歷史:

2003年時,正是delphi,vb一類的smart ui程式大行其道,Java在那個年代,還在使用jsp來完成大量的業務邏輯操作,4000行的jsp是常見的事;2005年spring hibernate替換了EJB,社群一片歡呼,所有人開始擁護action,service,dao這樣的貧血模型(充血模型,貧血模型會在下文論述);2007年,Rails興起,有人發現了Rails的activeRecord是漲血模型,引起了一片混戰;直到現在的2017年,微服務成為主流系統架構。

在現在這個年代,不懂個MVC分層,都不好意思說自己是搞java的,也不會有人在jsp裡面寫業務程式碼了
(可以說模板技術freemarker,thymeleaf已經取代jsp了),
但是在那個年代,還沒有現在這麼普遍地強調分層架構的重要性。

這個章節其實並不重要,因為mvc一類的分層架構已經是大多數java初學者的“起點”了,大多數DDD的文章都不會贅述這一點,我這裡列出來是為了讓大家知曉這篇文章的時代侷限性,在後續章節的理解中,也需要抱有這樣的邏輯:這本書寫於2004年。

模式: Entity與Value Object

我在不瞭解DDD時,就對這兩個術語早有耳聞。entity又被稱為reference object,我們通常所說的Java bean在領域中通常可以分為這兩類,(可別把value object和常用於前臺展示的view object,vo混為一談) 
entity的要義在於生命週期和標識,value object的要義在於無標識,通常情況下,entity在通俗意義上可以理解為資料庫的實體,(不過不嚴謹),value object則一般作為一個單獨的類,構成entity的一個屬性。

舉兩個例子來加深對entity和value object的理解。

例1:以電商微服務系統中的商品模組,訂單模組為例。將整個電商系統劃分出商品和訂單兩個限界上下文(Bound Context)應該是沒有爭議的。如果是傳統的單體應用,我們可以如何設計這兩個模組的實體類呢? 
會不會是這樣?

複製程式碼

class Product{
    String id;//主鍵
    String skuId;//唯一識別號
    String productName;
    Bigdecimal price;
    Category category;//分類
    List<Specification> specifications;//規格 
    ... 
}

class Order{
    String id;//主鍵
    String orderNo;//訂單號
    List<OrderItem> orderItems;//訂單明細
    BigDecimal orderAmount;//總金額
    ...
}

class OrderItem{
    String id;
    Product product;//關聯商品
    BigDecimal snapshotPrice;//下單時的價格
}

複製程式碼

 

看似好像沒問題,考慮到了訂單要儲存下單時候的價格(當然,這是常識)但這麼設計卻存在諸多的問題。
在分散式系統中,商品和訂單這兩個模組必然不在同一個模組,也就意味著不在同一個網段中。
上述的類設計中直接將Product的列表儲存到了Order中,也就是一對多的外來鍵關聯。這會導致,每次訪問訂單的商品列表,都需要發起n次遠端呼叫

反思我們的設計,其實我們發現,訂單BC的Product和商品BC的Product其實並不是同一個entity,在商品模組中,我們更關注商品的規格,種類,實時價格,這最直接地反映了我們想要買什麼的慾望。而當生成訂單後,我們只關心這個商品買的時候價格是多少,不會關心這個商品之後的價格變動,還有他的名稱,僅僅是方便我們在訂單的商品列表中定位這個商品。

如何改造就變得明瞭了

複製程式碼

class OrderItem{
    String id;
    String productId;//只記錄一個id用於必要的時候發起command操作
    String skuId;
    String productName;
    ...
    BigDecimal snapshotPrice;//下單時的價格
}

複製程式碼

 

是的,我們做了一定的冗餘,這使得即使商品模組的商品,名稱發生了微調,也不會被訂單模組知曉。這麼做也有它的業務含義,使用者會聲稱:我買的時候他的確就叫這個名字。記錄productId和skuId的用意不是為了查詢操作,而是方便申請售後一類的命令操作(command)。

在這個例子中,Order 和 Product都是entity,而OrderItem則是value object(想想之前的定義,OrderItem作為一個類,的確是描述了Order這個entity的一個屬性集合)。關於標識,我的理解是有兩層含義,第一個是作為資料本身儲存於資料庫,主鍵id是一個標識,第二是作為領域物件本身,orderNo是一個標識,對於人而言,身份證是一個標識。而OrderItem中的productId,id不能稱之為標識,因為整個OrderItem物件是依託於Order存在的,Order不存在,則OrderItem沒有意義。

例子2: 汽車和輪胎的關係是entity和value object嗎? 
這個例子其實是一個陷阱題,因為他沒有交代限界上下文(BC),場景不足以判斷。對於使用者領域而言,的確可以成立,汽車報廢之後,很少有人會關心輪胎。
輪胎和發動機,雨刮器,座椅地位一樣,只是構成汽車的一些部件,和使用者最緊密相關的,只有汽車這個entity,輪胎只是描述這個汽車的屬性(value object);
場景切換到汽修廠,無論是汽車,還是輪胎,都是汽修廠密切關心的,每個輪胎都有自己的編號,一輛車報廢了,可以安置到其他車上,這裡,他們都是entity。

這個例子是在說明這麼一個道理,同樣的事物,在不同的領域中,會有不同的地位。 

圖2:《領域驅動設計》Value Object模式的示例

在單體應用中,可能會有人指出,這直接違背了資料庫正規化,但是領域驅動設計的思想正如他的名字那樣,不是基於資料庫的,而是基於領域的
微服務使得資料庫發生了隔離,這樣的設計思想可以更好的指導我們優化資料庫。

 

模式: Repository

哲學家分析自然規律得出規範,框架編寫者根據規範制定框架。有些框架,可能大家一直在用,但是卻不懂其中蘊含的哲學。

——來自於筆者的口胡

記得在剛剛接觸mvc模式,常常用DAO層表示持久化層,在JPA+springdata中,抽象出了各式各樣的xxxRepository,與DDD的Repository模式同名並不是巧合,
jpa所表現出的正是一個充血模型(如果你遵循正確的使用方式的話),可以說是領域驅動設計的一個最佳實踐。

開宗明義,在Martin Fowler理論中,有四種領域模型: 
1. 失血模型 
2. 貧血模型 
3. 充血模型 
4. 脹血模型 
詳細的概念區別不贅述了,可以參見專門講解4種模型的部落格。他們在資料庫開發中分別有不同的實現,用一個修改使用者名稱的例子來分析。

class User{
    String id;
    String name;
    Integer age;
}

失血模型: 
跳過,可以理解為所有的操作都是直接操作資料庫,在smart ui中可能會出現這樣的情況。

貧血模型:

複製程式碼

class UserDao {
    @Autowired
    JdbcTemplate jdbcTemplate;

    public void updateName(String name,String id){
        jdbcTemplate.excute("update user u set u.name = ? where id=?",name,id);
    }
}

class UserService{

    @Autowired
    UserDao userDao;

    void updateName(String name,String id){
        userDao.updateName(name,id);
    } 
}

複製程式碼

貧血模型中,dao是一類sql的集合,在專案中的表現就是寫了一堆sql指令碼,與之對應的service層,則是作為Transaction Script的入口。
觀察仔細的話,會發現整個過程中user物件都沒出現過

充血模型:

複製程式碼

interface UserRepository extends JpaRepository<User,String>{
    //springdata-jpa自動擴展出save findOne findAll方法
}

class UserService{
    @Autowoird
    UserRepository userRepository;

    void updateName(String name,String id){
        User user = userRepository.findOne(id);
        user.setName(name);
        userRepository.save(user);
    }
}

複製程式碼

充血模型中,整個修改操作是“隱性”的,對記憶體中user物件的修改直接影響到了資料庫最終的結果,不需要關心資料庫操作,只需要關注領域物件user本身。Repository模式就是在於此,遮蔽了資料庫的實現。與貧血模型中user物件恰恰相反,整個流程沒有出現sql語句。

漲血模型: 
沒有具體的實現,可以這麼理解:

void updateName(String name,String id){
    User user = new User(id);
    user.setName(name);
    user.save();
}
 

我們在Repository模式中重點關注充血模型。
為什麼前面說:如果你遵循正確的使用方式的話,springdata才是對DDD的最佳實踐呢?
因為有的使用者會寫出下面的程式碼:

複製程式碼

interface UserRepository extends JpaRepository<User,String>{

    @Query("update user set name=? where id=?")
    @Modifying(clearAutomatically = true)
    @Transactional
    void updateName(String name,String id);
}

複製程式碼

歷史的車輪在滾滾倒退。本節只關注模型本身,不討論使用中的一些併發問題,再來聊聊其他的一些最佳實踐。

複製程式碼

interface UserRepository extends JpaRepository<User,String>{

    User findById();//√  然後已經存在findOne了,只是為了做個對比
    User findBy身份證號();//可以接受
    User findBy名稱();//×
    List<許可權> find許可權ByUserId();//×
}

複製程式碼

理論上,一個Repository需要且僅需要包含三類方法loadBy標識,findAll,save(一般findAll()就包含了分頁,排序等多個方法,算作一類方法)。
標識的含義和前文中entity的標識是同一個含義,在我個人的理解中,身份證可以作為一個使用者的標識(這取決於你的設計,同樣的邏輯還有訂單中有業務含義的訂單編號,保單中的投保單號等等),在資料庫中,id也可以作為標識。findBy名稱為什麼不值得推崇,因為name並不是User的標識,名字可能會重複,只有在特定的現場場景中,名字才能具體對應到人。
那應該如何完成“根據姓名查詢可能的使用者”這一需求呢?最方便的改造是使用Criteria,Predicate來完成檢視的查詢,哪怕只有一個非標識條件。
在更完善的CQRS架構中,檢視的查詢則應該交由專門的View層去做,可以是資料庫,可以是ES
findByUserId不值得推崇則是因為他違背了聚合根模式(下文會介紹),
User的Repository只應該返回User物件。

軟體設計初期,你是不是還在猶豫:是應該先設計資料庫呢,還是應該設計實體呢?在Domain-Driven的指導下,你應當放棄Data-Driven。

模式 聚合和聚合根

難住我的還有英文單詞,初識這個概念時,忍不住發問:Aggregate是個啥。文中使用聚合的概念,來描述物件之間的關聯,採用合適的聚合策略,可以避免一個很長,很深的物件引用路徑。對劃分模組也有很大的指導意義。

在微服務中我們常說劃分服務模組,在領域驅動設計中,我們常說劃分限界上下文。在面向物件的世界裡,用抽象來封裝模型中的引用,聚合就是指一組相關物件的集合,我們把它作為資料修改的單元。每個聚合都有一個聚合根(root)和一個邊界(boundary)。邊界定義了聚合內部有什麼,而根則是一個特定的entity,兩個聚合之間,只允許維護根引用,只能通過根引用去向深入引用其他引用變數。

例子還是沿用電商系統中的訂單和商品模組。在聚合模式中,訂單不能夠直接關聯到商品的規格資訊,如果一定要查詢,則應該通過訂單關聯到的商品,由商品去訪問商品規格。在這個例子中,訂單和商品分別是兩個邊界,而訂單模組中的訂單entity和商品模組中的商品entity就是分別是各自模組的root。遵循這個原則,可以使我們模組關係不那麼的盤根錯節,這也是眾多領域驅動文章中不斷強調的劃分限界上下文是第一要義。

模式 包結構

微服務有諸多的模組,而每個模組並不一定是那麼的單一職責,比模組更細的分層,便是包的分層。
我在閱讀中,隱隱覺得這其中蘊含著一層哲學,但是幾乎沒有文章嘗試解讀它。
領域驅動設計將其單獨作為了一個模式進行了論述,篇幅不小。重點就是論述了一個思想:包結構應當具有高內聚性

這次以一個真實的案例來介紹一下對高內聚的包結構的理解,專案使用maven多module搭建。
我曾經開發過一個簡訊郵件平臺模組,它在整個微服務系統中有兩個職責,
一:負責為其他模組提供簡訊郵件傳送的遠端呼叫介面,
二:有一個後臺頁面,可以讓管理員自定義傳送簡訊,並且可以瀏覽全部的一,二兩種型別傳送的簡訊郵件記錄。

在設計包結構之前,先是設計微服務模組。

api層定義了一系列的介面和介面依賴的一些java bean,model層也就是我們的領域層。
這兩個模組都會打成jar包,外部服務依賴api,api則由app模組使用rpc框架實現遠端呼叫。
admin和app連線同一個資料來源,可以查詢出簡訊郵件記錄,admin需要自定義傳送簡訊也是通過rpc呼叫。
簡單介紹完了這個專案後,重點來分析下需求,來看看如何構建包結構。 

mvc分層天然將controller,service,model,config層分割開,這符合DDD所推崇的分層架構模式
(這個模式在原文中有描述,但我覺得和現在耳熟能詳的分層結構沒有太大的出入,所以沒有放到本文中介紹),
而我們的業務需求也將簡訊和郵件這兩個領域拆分開了。
那麼,
到底是mvc應該包含業務包結構呢?
還是說業務包結構包含mvc呢?

mvc高於業務分層

//不夠好的分層
sinosoftgz.message.admin
    config
        CommonConfig.java
    service
        CommonService.java
        mail
            MailTemplateService.java
            MailMessageService.java
        sms
            SmsTemplateService.java
            SmsMessageService.java
    web
        IndexController.java
        mail
            MailTemplateController.java
            MailMessageController.java
        sms
            SmsTemplateController.java
            SmsMessageController.java
    MessageAdminApp.java

業務分層包含mvc

複製程式碼

//高內聚的分層
sinosoftgz.message.admin
    config
        CommonConfig.java
    service
        CommonService.java
    web
        IndexController.java
    mail
        config
            MailConfig.java
        service
            MailTemplateService.java
            MailMessageService.java
        web
            MailTemplateController.java
            MailMessageController.java
    sms
        config
            Smsconfig.java
        service
            SmsTemplateService.java
            SmsMessageService.java
        web
            SmsTemplateController.java
            SmsMessageController.java
    MessageAdminApp.java

複製程式碼

業務並不是特別複雜,但應該可以發現第二種(業務分層包含mvc)的包結構,才是一種高內聚的包結構。
第一種分層會讓人有一種將各個業務模組(如mail和sms)的service和controller隔離開了的感覺,當模組更多,每個模組的內容更多,這個“隔得很遠”的不適感會逐漸侵蝕你的開發速度。
一種更加低內聚的反例是不用包分層,僅僅依賴字首區分,由於在專案開發中真的發現同事寫出了這樣的程式碼,我覺得還是有必要拿出來說一說:

//反例
sinosoftgz.message.admin
    config
        CommonConfig.java
        MailConfig.java
        Smsconfig.java
    service
        CommonService.java
        MailTemplateService.java
        MailMessageService.java
        SmsTemplateService.java
        SmsMessageService.java
    web
        IndexController.java
        MailTemplateController.java
        MailMessageController.java
        SmsTemplateController.java
        SmsMessageController.java     
    MessageAdminApp.java

這樣的設計會導致web包越來越龐大,逐漸變得臃腫,使專案僵化,專案經理為何一看到程式碼就頭疼,
規範的高內聚的包結構,遵循業務>mvc的原則,可以知道我們的專案龐大卻有條理。

其他模式

《領域驅動設計》這本書介紹了眾多的模式,上面只是介紹了一部分重要的模式,後續我會結合各個模式,儘量採用最佳實踐+淺析設計的方式來解讀。

 

微服務之於領域驅動設計的一點思考

技術架構誠然重要,但不可忽視領域拆解和業務架構,《領域驅動設計》中的諸多失敗,成功案例的總結,是支撐其理論知識的基礎,最終匯聚成眾多的模式。在火爆的微服務架構潮流下,我也逐漸意識到微服務不僅僅是技術的堆砌,更是一種設計,一門藝術。我的本科論文字想就微服務架構進行論述,奈何功底不夠,最後只能改寫成一篇分散式網站設計相關的文章,雖然是一個失敗的過程,但讓我加深了對微服務的認識。如今結合領域驅動設計,更加讓我確定,技術方案始終有代替方案,決定微服務的不是框架的選擇,不僅僅是restful或者rpc的介面設計風格的抉擇,而更應該關注拆解,領域,限界上下文,聚合根等等一系列事物,這便是我所理解的領域驅動設計對微服務架構的指導意義。