DDD & DDDLib在恒拓開源的發展歷程與推廣經驗

作者: 劉林  來源: InfoQ  發布時間: 2014-01-01 17:52  閱讀: 7198 次  推薦: 6   原文鏈接   [收藏]  

  領域驅動設計(DDD)的概念源于2004年著名建模專家Eric Evans發表的書籍:《Domain-Driven Design – Tackling Complexity in the Heart of Software》中文譯名:領域驅動設計—軟件核心復雜性應對之道,池建強在2011年發表的一篇文章《領域驅動設計和實踐》中是這樣形容DDD的:

領域驅動設計事實上是針對OOAD的一個擴展和延伸,DDD基于面向對象分析與設計技術,對技術架構進行了分層規劃,同時對每個類進行了策略和類型的劃分。

  本文主要介紹為什么我們在恒拓開源內部推廣DDD,我們如何通過開發 DDDLib 和 Koala 等工具來完善這一過程,推廣過程中遇到了哪些問題,以及我們如何解決這一問題。

  為什么選擇DDD

  傳統的模式的最大優點在于開發人員非常熟悉,開發成本低,但它也有一些問題:

  采用DDD開發模式之前,傳統的開發模型是最流行的Model-Dao-Service-UI開發模型,通常是基于事務腳本(Transaction Script)和表模塊(Table Module)模式的實現,這種模式通常是先設計表,再建模,實現容易依賴特定的表的一些特性,如存儲過程。基于表的設計模式容易帶來以下幾個問題:

  • 業務建模完全是表的復制,無法真實反映業務。
  • 核心業務分散在各個地方,非常危險,修改擴展難,且難以閱讀。

  這種開發模式適合一些需求小,后續維護擴展需求小的中小型項目,但在大型企業級系統或產品,擴展維護或需求變量非常多的情況下,缺點也非常明顯。

  相對而言,DDD則有以下四點好處:

  1、面向對象,模型真實反映業務現實:使用DDD領域驅動設計,模型通常是業務的真實反映,業務集中在領域而不是分散在各Service中,有利于對業務的理解。

  2、使用領域統一建模語言:有利于業務溝通與建模: DDD倡導先對業務建模,而非關注表或腳本的設計;在建模過程中,由于領域本身是對真實業務的反映與建模,因此與業務專家更容易溝通,打破技術與業務的溝通隔閡。

  3、可重用性高:DDD中,領域層為核心,每個領域對象都是一個相對完整的內聚的業務對象描述,所以可以形成直接的復用。基于領域建模的設計,并不會依賴特定的數據庫及特性,模型是可以完全重用且沒有技術上的沖突。

  4、業務越復雜,DDD的優勢越明顯:領域模型采用OO設計,通過將職責分配到相應的模型對象或Service,可以很好的組織業務邏輯,當業務變得復雜時,領域模型顯出巨大的優勢。

  DDDLib登場

  DDD本質上是一種思想,并不是新技術。在恒拓開源,由我們的楊宇老師和陳操總共同創作的DDDLib庫,是對DDD思想的核心支持與實現。

  DDDLib是一整套支持DDD思想實現的類庫,DDDLib下還是使用的 Hibernate、JPA或MyBatis、noSQL等技術為實現。

  如同DDD所要求樣,使用DDDLib 的項目分層圖為:

  • 用戶界面/展現層
    • 用于向用戶展現信息,處理用戶在界面上的請求,比如struts,tapestry,springMVC等頁面框架。
  • 應用層
    • 用來處理應用的活動,不包含領域中的業務邏輯。
    • 應用層可以用來處理一些與領域概念無關的攔截性質的工作,比如日志,事務等。此外,應用層也可以用來處理一些既不屬于展現層,也不屬于領域層,而是屬于目前應用相關的一些服務。比如資金轉賬的業務的讀取輸入功能(讀取輸入不是轉賬的核心業務含義)。
  • 領域層
    • 此層是DDD的核心:領域對象,領域服務,倉儲接口均位于此層。
    • 領域的信息,是業務軟件的核心所在。
    • 需要保留業務對象的狀態,對業務對象及其狀態持久化的操作交給基礎設施層。
    • 領域層應該遵從以下原則:除非業務發生變化,否則其他任何變化均不應該影響到領域層。這些其他變化包括:不同的展現框架,不同的頁面展現內容,是否要分頁,是否支持手機客戶端,是否公開WebService,是否提供OpenAPI等等。
  • 基礎設施層
    • 此層作為其他層次的支撐,可以為領域層的持久化提供支持,當領域層或應用層有特殊的業務或應用需求(發送短信等)時,它們會定義需求接口,然后在基礎設施層中實現這個接口,滿足特定的業務或應用需求。

  DDDLib的核心實現如下:

  上圖就是使用 DDDLib 項目的整體技術架構圖,也表明了DDDLib的整體原則:

  1. 領域層是業務核心,這一層不依賴任何特定的技術框架,保證它的業務純潔性。DDDLib中的領域層只依賴JDK、DDDLib的Domain庫以及倉儲接口及其它自定義接口。
  2. 使用倉儲和查詢通道作為與存儲介質相關的操作接口,隔離對特定數據庫技術或存儲介質的依賴。
  3. 提供多種不同的IoC容器實現及InstanceFactory實例工廠,隔離對特定IOC 技術的依賴。
  4. 領域層包括值對象、領域對象以及領域服務三個要素,領域層不是數據庫操作層而是業務建模層。許多開發者在使用的過程中,最終還是把領域層作為數據庫操作層來使用,對實體的方法也是以數據庫的操作行為為標準 ,如查詢,新增,刪除一個實體,最終依然回歸到以數據庫為中心的方向去了,這是需要避免的。
  5. 改變以數據庫為中心的核心是意識到業務行為才是核心,數據庫存儲是支撐。意識到數據庫是支撐非常關鍵,業務上的任何行為,在系統中最終需要存儲記錄,數據庫存儲是對業務實現的支撐,也可以使用文件,緩存或云空間等其它存儲介質。想像一下,使用數據庫進行設計的項目,最終就限定了存儲介質為特定的數據庫,如果下一次需要更換為云空間或緩存等其它存儲形式,就會發現整個系統需要重新設計開發,但使用DDDLib,只需更換倉儲實現,提供一個云空間的實現就行了,核心業務邏輯完全不需要變動與修改。

  DDDLib在實現過程中也經歷了內部的不少爭議,經過很多次的討論和打磨形成了現在的格局。在下面的部分,我將介紹DDDLib在幾個重要組件上的實現細節。

  DDDLib倉儲的實現

  從DDDLib 1.0到3.5版本,倉儲實現歷經幾個階段,分別是:

  1. 給每個領域對象定義一個倉儲接口及一個倉儲實現。

    這種倉儲實現非常受爭議,開發人員并不認可這種方式,倉儲接口及實現非常多,一方面導致項目類太多,并且也帶來編碼的重復操作。

  2. 與spring data整合,給每個領域對象定義倉儲接口,無須定義實現。這種模式對前面的模式有了優化,只定義接口不定義實現,但是spring data這種依賴方法名,參數來進行查詢的模式,針對一些復雜的查詢,難以勝任。
  3. 提供默認的hibernate及JPA的通用倉儲接口。

    為每個倉儲定義一個接口,這種模式慢慢的不被接受,使用spring data帶來的優化方案,也有非常多的問題,后面根據JPA的實體管理思路,于是形成了通用倉儲接口及不同技術實現的思路,定義一個通用的倉儲接口,包括通用的增刪改查數據庫行為。

  4. 支持MyBatis的通用倉儲接口

    DDDLib的JPA及hibernate倉儲實現,這個方案是一個較佳的方案,所有領域實體使用通用的倉儲,避免了大量重復代碼,但DDDLib一直是基于Hibernate/JPA提供的實現與技術支持,在項目的使用過程中,經常會遇到不適合使用Hibernate/JPA模式的項目,對MyBatis的需求也非常大,這種情況下,Koala團隊定義實現了MyBatis的倉儲實現,并保證其與JPA/Hibernate模式下API的一致性。

  DDDLib中的DTO

  DTO,數據傳輸對象,領域對象雖然有數據(屬性),但是領域對象上面還帶有操作,在某些場合不適合進行傳輸,有些時候傳輸還需要序列化。但并不是所有的領域對象屬性都可以暴露,而且有些屬性可能要合并,可能要分解,之后才有利于前端的使用。于是就有了專門用來傳輸數據的DTO,只有屬性,沒有操作,必要的時候加上序列化標記,實現遠程調用。

  這是DDD中DTO的作用,但是DTO同時也帶來了實體與DTO的轉換性能問題,在大數據量下尤其明顯。

  DDDLib中的數據庫支撐行為類

  在DDDLib實現中,提供了Repository以及QueryChannelService兩個接口,分別使用在領域層以及應用層,都是對數據庫的操作接口。

  SQL/HQL/JPQL寫在哪

  在使用DDDLib的過程中,不同的持久層框架的SQL語言不一樣,比如MyBatis使用的是SQL,Hibernate使用的是 HQL,JPA下使用的是JPQL。

  這些語句寫在哪兒在公司也經歷過一番爭議與變更,歷史如下:

  1、寫在代碼中

public static Resource newResource(String name,String identifier,String level,String menuIcon){
  Resource resource = null;
  List<Resource> resources = Resource.getRepository().find("select r from Resource r where r.name = ? " + "and r.identifier = ?", new Object[]{name,identifier}, Resource.class);
  ...
} 

  這個非常容易理解:哪里要查詢,哪里寫SQL,這種模式是公司最初的使用方式,優缺點也非常明顯。優點是直觀,缺點則是,DDDLib倡導隔離特定技術,那這些語句寫在寫到代碼中,意味著領域層中依賴了特定的技術語句,違背了DDDLib的目標。

  2、以@NamedQuery注解寫在類上

@Entity
@NamedQuery(name="findAllEmployeesByFirstName",queryString="SELECT OBJECT(emp) FROM Employee emp WHERE emp.firstName = 'John'")
public class Employee implements Serializable {
  ...
} 

  在Koala產品出現以前,DDDLib支持Hibernate以及JPA,這兩種模式都支持@NamedQuery這種注解。曾經有人提議過這種方式,但并沒有解決上面的問題,反而帶來難以閱讀的問題。

  3、以命名查詢+xml實現

/**
* 判斷一個資源是否有子資源
*/
public static boolean hasChildByParent(Long parentId) {
  return !Resource.findByNamedQuery("hasChildByParent", new Object[] { parentId }, Resource.class).isEmpty();
} 

  在對應的 XML 配置中定義:

<named-query name="hasChildByParent">
<query>
select distinct m.id from ResourceLineAssignment m where m.parent.id=?
</query>
</named-query>  

  Koala團隊后期定制了我們的方式,使用命名查詢+xml配置方式來實現,并且基于這種方式,提供了MyBatis協議的支持實現,獲得了一定的認可。

  使用這種方式,領域層的代碼中不會出現特定的技術語句,解決了上述問題,不同的技術實現下不同的xml配置這種模式是可以接受的。

  但這種模式也仍有一些問題,包括:

  a) 動態條件查詢不支持。一些查詢的條件是動態的,這種模式下的HQL/JPQL是不支持的,但由于通常動態條件查詢不屬于業務核心,而是應用需求是在應用層,因此暫時仍然可以接受。

  b) 擴展其它存儲介質的問題。Hibernate/JPA/MyBatis是常用的持久層框架,但是現在越來越多的出現諸如NoSQL、云存儲等非關系型存儲,使用這種模式來匹配這些存儲,當前沒有現成的解決方案

  4、不使用語句,而使用QueryCriterion

  DDDLib中提供了QueryCriterion這種查詢機制,以期望做到隔離特定持久層框架的影響。很可惜,早期在恒拓推廣使用 DDDLib 時,沒有提及這種實現,因此恒拓的DDD一直未使用此方案。

  這種方式對于MyBatis難以支持,對于NoSQL、云存儲等其它存儲介質同樣沒有現成的解決方案。

  DDDLib最終使用了第三種方案。

  JPA下的寫法:

  MyBatis下的寫法:

  推廣DDD遇到的挑戰

  上面闡述了傳統開發模式的一些問題,但是從另一個角度來說,傳統的開發模式也有著它的優點:

  1. 使用成本低:基于傳統開發方法,使用SSH等框架,基本上所有的開發人員都沒有上手代價,立馬可以參與開發。
  2. 開發速度快:傳統模式的開發速度也快,尤其使用一些快速開發框架,開發一些簡單業務的項目,是比較合適的。
  3. 在實際項目的開發中,傳統模勢的缺點并不明顯,國內大部分項目只注重實際頁面效果,至于設計的好壞,維護擴展的難易度并沒有被重視。

  基于這些理解,在公司層面推廣使用DDD思想存一些難點與阻礙,尤其是公司早期一些項目,使用DDD的所帶來的優勢并不明顯,反而前期會帶來使用成本。

  另一方面,對大部分開發人員來說,傳統的模式更容易接受和習慣,表->DAO->Services->UI這種非常簡單,基本沒有學習成本。推廣使用DDD則不一樣,雖然技術上并沒有任何創新,使用的還是Hibernate、Struts、Spring這些常用的技術,但關鍵是編碼的思想不一樣。DDD的要點,領域對象,領域服務,查詢通道這些讓開發人員理解并熟悉這種編碼風格,也是個不小的挑戰,推廣初期經常會遇到開發人員對這些理念的質疑。

  另外,雖然DDD倡導先對領域進行分析建模,編寫領域方法等核心,再考慮表的設計實現,但在實際的開發過程中,受以往的模式影響,就算使用了DDD模式,開發人員經常還是會用傳統方式開發,對新模式無法理解,如:

  • 開發人員還是先設計表,再根據表設計領域對象。許多項目依然還是對實體進行增刪改查,于是出現DTO與實體屬性一模一樣的情況。開發人員對DTO的存在的必要性無法理解。
  • 在傳統的模式中,操作數據庫的基本上屬于Dao的責任。DDDLib提供了兩個數據查詢通道,開發人員對于哪些需要使用倉儲,哪些需要使用查詢通道不能清晰理解。
  • DDDLib提供了值對象,領域對象,領域服務來將領域與值進行分離,但是在實際的項目中,許多開發人員在項目使用領域對象,然后直接針對領域對象增刪改查,很少真正使用到值對象、領域服務、聚合、工廠這些要素。

  以上種種讓 DDD 又變成傳統方式,這又進一步讓開發人員感覺DDD與傳統模式沒有本質區別,于是加劇了開發人員對使用 DDD 的疑問。

  如何解決這些問題

  • 在公司內部推廣宣傳DDD&DDDLib

    DDD更多的是理念,技術細節和傳統的方式沒有區別,更多的是要讓開發人員在理念上轉變過來,因此推廣DDD&DDDLib及其理念非常重要。不理解DDD思想理念來使用DDD,最終非常容易造成難以使用,這有什么用,或為什么要用這樣的感覺。

  • 定期進行DDD在技術上的交流與探討

    如前文所述,DDDLib的一些技術實現細節非常有爭議性,因此我們會定期召集有想法的人員,不斷討論,收集使用者的意見,不斷的修正優化實現。

    對于有爭議的技術,也通過討論,集思廣益,找到最佳的實現方式。

  • 制定最佳編碼實踐,指導DDD方式的編碼

    考慮到開發人員從傳統模式轉變到DDDLib編碼風格的轉變及學習成本問題,通常公司的架構部都會編寫一個最佳DEMO,示例大家如何編寫代碼,理解查詢通道,值對象,DTO,倉儲,聚合等的實現與細節,幫助開發人員正確編寫。

  • 通過真實項目逐步讓大家認識到DDD的優點

    在一些項目上,使用 DDD 并沒有實際的優點,只有在業務復雜,需要不斷迭代的如產品這樣的項目,才比較適合 DDD。我們通常會拿真實的項目中的代碼來示例說明使用DDD的好處。

  • 開發一套基于DDD思想的開發平臺,輔助更容易使用DDD編碼

    在使用了較長的 DDDLib 之后,恒拓也推出了基于它的思想的Koala開發平臺,借助這個開發平臺,能更容易讓開發者上手使用DDD。

  總結

  到目前,公司的大部分項目已經在使用DDDLib&Koala平臺來實現。開發人員慢慢接受了這種思想,并且有許多人在DDDLib實現上提出了自己的意見與想法,包括倉儲沒有native SQL支持,NoSQL如何實現,DTO是否有必要,沒有體現聚合,領域層是否可使用工廠,父實體需不需要基本屬性(創建時間,創建者)等。

  接下來,我們計劃在以下方面繼續展開工作:

  1) 完善支持技術細節

  包括對NoSQL的倉儲實現的支持,較好的支持非Spring IOC等,技術細節上仍然需要優化。

  2) 制定更為優化的DDDLib標準規范

  包括倉儲,查詢通道,聚合等理念,為它們提供更為優化的技術實現標準,更好的支持DDD設計。

  3) 完善基于DDDLib的Koala開發平臺

  進一步完善 Koala開發平臺,更好的支持DDD&DDDLib理念的實現。

  4) 推廣DDD理念

  公司借助Koala這個開源平臺,以讓更多的人嘗試并加入到使用 DDD或 DDDLib,推動國內做出更優秀的應用

  相關資料

  作者介紹

  劉林,恒拓開源架構師,對技術非常熱愛與執著,關注開源技術及開源的解決方案,信奉并參與開源。在Java開發領域有一定的經驗積累。參與并主導過一些開源項目的成功實施。現專注于Koala開源平臺的開發。

6
1
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()