我的“第一次”,就這樣沒了:DDD(領域驅動設計)理論結合實踐


寫在前面

      插一句:本人超愛落網-《平凡的世界》這一期,分享給大家。

  閱讀目錄:

第一次聽你,清風吹送,田野短笛;第一次看你,半彎新湖,魚躍翠堤;第一次念你,燕飛巢冷,釋懷記憶;第一次夢你,雲翔海島,輪渡迤邐;第一次認你,怨江別續,草橋知己;第一次怕你,命懸一線,遺憾禁忌;第一次悟你,千年菩提,生死一起。

  人生有很多的第一次:小時候第一次牙牙學語、第一次學蹣跚學步。。。長大后第一次上課、第一次逃課、第一次騎自行車、第一次懂事、第一次和喜歡的人說“我愛你”、第一次旅行、第一次敞開心扉去認識這個世界。。。

  第一次的感覺:有甜蜜、有辛酸;有勇敢、有羞澀;有成功、有失敗。不管怎樣,都要勇敢的邁出第一步,不論成功與失敗,至少自己努力過,證明過自己就好,就像哥倫布探索美洲一樣,沒有勇敢邁出第一步,也許現在“美洲”的概念會推遲不知多少年。

  以下內容,只是一些個人看法和實現,僅供參考學習,也歡迎討論指教。

關於DDD

  對DDD(領域驅動設計)最初的了解,始於這一篇博文:http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html,當時花了四五個小時閱讀完,但只是初步對DDD有個了解,有點顛覆自己對編程思想的看法。2004年 Eric Evans 發表 Domain-Driven Design –Tackling Complexity in the Heart of Software (領域驅動設計- 軟件核心復雜性應對之道),簡稱Evans DDD,這本書網上一直沒有買到,很遺憾,如果有的朋友有珍藏,可以高價收購。

  什么是DDD(領域驅動設計)?DDD中最核心的是Domain Model(領域模型),和領域模型相對的是事務腳本,領域模型和事務腳本說到底就是面向對象和面向過程的區別。

  • 事務腳本:圍繞功能,以功能為中心。將所有邏輯組織在一個單一過程,進行數據庫直接調用,每筆交易(業務請求)都有自己的事務腳本,並且是一個類的公開方法。
  • 領域模型:描述領域類,以類之間的協作完成所需功能。所謂領域模型,是一系列相互關聯的對象,每個對象代表一定意義的獨立體,既可以一起以一種大規模方式協作;也可以小到以單線方式運行。

  好像有個報告統計,大約80%的程序員使用事務腳本編程,三層架構(UI、BLL、DAL)對於我們來說太熟悉了,編程的時候代碼一般會集中在DAL層,致使數據訪問層充斥着大量的業務邏輯,而且很難復用,每個DAL中的類就像一個單元,只為某一功能實現,也就是上面所說的“單一過程”,因為業務邏輯都實現在數據訪問層了,這樣業務邏輯層就成了一個空架子,有的人就會覺得BLL-業務邏輯層沒有存在的必要,然后設計的時候就把業務邏輯層去掉了,就只剩UI和DAL層了,外加一些HelpClass,然后的然后。。。

  領域驅動設計的概念從提出到現在十年了,現在很少的公司能真正的去應用,而還是采用事務腳本的方式,為什么?其實就是一種思想,或者說方式的轉變,就好比你以前習慣用手直接吃飯,現在讓你拿筷子吃飯,肯定會不習慣。當然還有一部分原因是領域驅動設計的推行,或者說國內有關這領域的大牛們很少,但我覺得不管怎樣,這是個趨勢,就像黑夜過后,一定會是清晨一樣。

  上面說到三層架構(UI、BLL、DAL),我們再看一下領域驅動設計的分層:

          來自:dax.net

主要分為四層(表現層、應用層、領域層和基礎層):

  • Presentation Layer:表現層,負責顯示和接受輸入;
  • Application Layer(Service):應用層,很薄的一層,只包含工作流控制邏輯,不包含業務邏輯;
  • Domain Layer(Domain):領域層,包含整個應用的所有業務邏輯;
  • Infrastructure Layer:基礎層,提供整個應用的基礎服務;

  領域驅動設計主張充血模型,也就是富模型的意思,大多業務邏輯都應該被放在Domain Object里面(包括持久化業務邏輯),而Service層應該是很薄的一層,僅僅封裝事務和少量邏輯,不和Dao層打交道。 

  優點:

  1. 更加符合OO的原則。
  2. Service層很薄,只充當Facade的角色,不和Dao打交道。

  缺點:

  1. Dao和Domain Object形成了雙向依賴,復雜的雙向依賴會導致很多潛在的問題。 
  2. 如何划分Service層邏輯和Domain層邏輯是非常含混的,在實際項目中,由於設計和開發人員的水平差異,可能導致整個結構的混亂無序。 (這個問題在項目實際運作的時候會出現,划分很重要。)
  3. 考慮到Service層的事務封裝特性,Service層必須對所有的Domain Object的邏輯提供相應的事務封裝方法,其結果就是Service完全重定義一遍所有的Domain Logic,非常煩瑣,而且Service的事務化封裝其意義就等於把OO的Domain Logic轉換為過程的Service TransactionScript。該充血模型辛辛苦苦在Domain層實現的OO在Service層又變成了過程式,對於Web層程序員的角度來看,和貧血模型沒有什么區別了。 (和第二點類似,如何做到Application層不包含業務邏輯,協調領域層和基礎層很重要。

  領域模型概念參照:http://www.oschina.net/question/12_21641

  領域驅動設計系列:http://www.cnblogs.com/daxnet/archive/2010/11/02/1867392.html

前期分析

  關於DDD(領域驅動設計)概念有一定了解后,下面開始做一個基於領域驅動設計的項目:MessageManager(短消息系統),至於為什么要拿短消息當小白鼠?是有原因的,當然隨便一個業務需求也是可以的,實踐是檢驗理論的唯一標准。

  MessageManager(后面就這樣命名)大概類似於博客園-短消息系統,用戶模塊暫不考慮,只考慮短消息,大致畫了一張功能分析圖:

  可能當你看到這張圖的第一反應是:Are you kidding me???對,你沒看錯,MessageManager功能就是這么簡單,其實領域驅動設計的項目應用應該是一些包含大型業務邏輯的,這種簡單的“CURD”操作很難體現出領域驅動設計的作用,但重點不是去實現,而是一個示例框架,可能設計不是很合理,但是一個完整的流程要走下來,當然領域驅動設計包含很多東西,不只是框架設計這一點,很不幸,本篇就只是討論的這一點。

  MessageManager數據分析圖:

  Are you kidding me again???對,你又沒看錯!!!數據庫設計就這么簡單,其實不應該說是數據庫設計,應該是領域模型設計-數據部分,主要體現在數據庫存儲,主要是兩個表:User(用戶表)和Message(消息表),注意我在畫圖的時候並沒有設計字段類型,只是字段名稱,類型設計應該在 Infrastructure Layer(基礎層)去實現,准確的來說應該是ORM,領域模型只是定義,並不包含實現,有時候我們在做設計的時候,比如ORM使用的是EntityFramework,采用的模式是:Database First,也就是dax.net所說的:

  EntityFramework中的從數據庫生成模型”功能應該去掉,但只是相對於領域驅動設計而言,如果項目采用事務腳本,你會發現這個功能是多么的方便,凡事都有相對性。后來EntityFramework推出“Code First”模式,這種模式就符合領域驅動設計思想,MessageManager就是采用這種方式。

  MessageManager的擴展圖:

  因為不考慮用戶模塊,所以用戶接入暫不考慮,只擴展一個消息接口,實現方式是:ASP.NET WebAPI,采用WebAPI主要原因是支持REST(無狀態),這里需要注意的是此接口雖然是服務,但是屬於Presentation Layer(表現層)。關於ASP.NET WebAPI可以參考:http://www.cnblogs.com/xishuai/p/3651370.html

  注:以上前期分析都是按照自己理解去完成,如果嚴格按照領域驅動設計,應該是建模專家按照嚴格的流程去做分析的,而不是像我這樣隨便畫幾張圖。

框架搭建

  MessageManager主要用到概念或技術點:EntityFramework、ASP.NET MVC、ASP.NET WebAPI、AutoMapper、Nunit、Unity、Unit Of Work、Repository、Specification等等。

  解決方案:

  主要分為四層,可以對比上面的領域驅動設計分層圖,當然復雜一點不只分為四層,但是這是最基本的,dax.net在 http://www.cnblogs.com/daxnet/archive/2011/05/10/2042095.html,一文中就增加了很多東西,示例圖:

                  來自:dax.net

  XXXX.Repositories項目dax.net在設計的時候放在了Domain中,也就是命名:XXXX.Domain.Repositories,但我覺得倉儲實現應該在Infrastructure(基礎層)中實現,Domain中只是定義倉儲契約,也就是Infrastructure(基礎層)中的MessageManager.Repositories,實現倉儲的具體實現,並提供持久化操作。

  工作流程描述可以用Unit Of Work一文中畫過一張圖表現:

點擊查看大圖

代碼實現

  MessageManager代碼編寫主要是四個方面:框架底層、功能實現、單元測試、前端頁面。

  框架底層實現可以結合上面那張圖和源碼去理解,前端頁面整理放在MessageManager.WebFiles項目中,頁面原始來自博客園-短消息系統,做了一點修改。這邊說下單元測試,關於單元測試可以參考:http://www.cnblogs.com/xishuai/p/3728576.html,因為我開發工具使用的是VS 2012,使用的是:NUnit Test Adapter,MessageManager項目中進行單元測試最重要的是Infrastructure(基礎層)和Application(應用層),Infrastructure(基礎層)主要是對MessageManager.Repositories項目進行單元測試,也就是測試項目:MessageManager.Repositories.Tests,測試主要包含倉儲持久化操作,如下:

  功能實現主要是領域模型設計、倉儲設計、應用層協調、表現層(MVC、WebAPI)代碼編寫等,當然還有一些應用程序配置,比如Automapper類型映射、Unity依賴注入配置等。說到領域模型設計,就多說一點,先了解領域模型涉及的概念:實體、值對象、聚合、聚合根。MessageManager項目包含兩個實體:User實體和Message(實體),當時設計的時候,我是把User作為實體、Message作為聚合根,也就是下面代碼:

/**
* author:xishaui
* address:https://www.github.com/yuezhongxin/MessageManager
**/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MessageManager.Domain.DomainModel
{
    public class Message : IAggregateRoot
    {
        #region 構造方法
        public Message()
        {
            this.ID = Guid.NewGuid().ToString();
        }
        #endregion
        
        #region 實體成員
        public string FromUserID { get; set; }
        public string FromUserName { get; set; }
        public string ToUserID { get; set; }
        public string ToUserName { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public DateTime SendTime { get; set; }
        public bool IsRead { get; set; }
        public virtual User FromUser { get; set; }
        public virtual User ToUser { get; set; }
        #endregion

        #region IEntity成員
        /// <summary>
        /// 獲取或設置當前實體對象的全局唯一標識。
        /// </summary>
        public string ID { get; set; }
        #endregion
    }
}

  Message繼承IAggregateRoot,User和Message組成一個消息聚合,聚合根為Message,訪問消息聚合內的成員,必須通過聚合根(Message)才能訪問,但是在做的過程中,有一個需求就是要通過用戶名獲取User,如果通過Message訪問就很不合理,因為這不包含任何的消息操作,所以后面就把User單獨作為一個聚合,聚合根為其本身,這邊說明的就是,聚合邊界划分不一定一成不變,需要根據具體的業務場景去划分,就比如:做User模塊的時候,Message就不能設計成聚合了,而應該是User。

  還有一點就是EntityFramework使用Code First的時候,因為我們“字段”都是設計在Domain層中(並不包含配置),實現卻是在Infrastructure層,如何進行數據庫字段類型設計?或是表字段關聯?實現主要是使用ModelConfigurations,在生成之前添加Model配置,我覺得這是EntityFramework在領域驅動設計開發中優點之一,設計和實現完全區分開,示例代碼:

 1 using System.ComponentModel.DataAnnotations;
 2 using System.Data.Entity.ModelConfiguration;
 3 using MessageManager.Domain.DomainModel;
 4 
 5 namespace MessageManager.Repositories.EntityFramework.ModelConfigurations
 6 {
 7     public class MessageConfiguration : EntityTypeConfiguration<Message>
 8     {
 9         /// <summary>
10         /// Initializes a new instance of <c>MessageConfiguration</c> class.
11         /// </summary>
12         public MessageConfiguration()
13         {
14             HasKey(c => c.ID);
15             Property(c => c.ID)
16                 .IsRequired()
17                 .HasMaxLength(36)
18                 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
19             Property(c => c.FromUserID)
20                 .IsRequired()
21                 .HasMaxLength(36);
22             Property(c => c.ToUserID)
23                 .IsRequired()
24                 .HasMaxLength(36);
25             Property(c => c.Title)
26                 .IsRequired()
27                 .HasMaxLength(50);
28             Property(c => c.Content)
29                 .IsRequired()
30                 .HasMaxLength(2000);
31             Property(c => c.SendTime)
32                 .IsRequired();
33             Property(c => c.IsRead)
34                 .IsRequired();
35             ToTable("Messages");
36 
37             // Relationships
38             this.HasRequired(t => t.FromUser)
39                 .WithMany(t => t.SendMessages)
40                 .HasForeignKey(t => t.FromUserID)
41                 .WillCascadeOnDelete(false);
42             this.HasRequired(t => t.ToUser)
43                 .WithMany(t => t.ReceiveMessages)
44                 .HasForeignKey(t => t.ToUserID)
45                 .WillCascadeOnDelete(false);
46         }
47     }
48 }

  上面代碼中的下面部分是添加外鍵配置,EntityFramework中的模型-添加配置:

1         protected override void OnModelCreating(DbModelBuilder modelBuilder)
2         {
3             modelBuilder
4                 .Configurations
5                 .Add(new UserConfiguration())
6                 .Add(new MessageConfiguration());
7             base.OnModelCreating(modelBuilder);
8         }

  下面再說下MessageManager.Application(應用層)的協調配置,先看下面的一張圖,注意后面所做的操作都是領域層或是基礎層去實現的,並不是應用層實現,應用層只是做協調處理,不要把應用層當做BLL(業務邏輯層)。

                        點擊查看大圖

開源-發布

  注:ASP.NET WebAPI 暫只包含:獲取發送放消息列表和獲取接收方消息列表。

  調用示例:

  WebAPI 客戶端調用可以參考 MessageManager.WebAPI.Tests 單元測試項目中的示例調用代碼。

  Web 示例頁面:

撰寫短消息:

發件箱:

查看/回復短消息:

  WebAPI 示例頁面:

后記

  關於時間成本:

  • MessageManager項目:兩天(包含晚上)+兩個晚上;
  • 本篇博客:一個下午+一個晚上(很晚)+外加更正無數;

  關於DDD實踐-MessageManager項目,有幾個問題需要記錄一下:

  • Domain Model(領域模型):領域模型到底該怎么設計?你會看到,MessageManager項目中的User和Message領域模型是非常貧血的,沒有包含任何的業務邏輯,現在網上很多關於DDD示例項目多數也存在這種情況,當然項目本身沒有業務,只是簡單的“CURD”操作,但是如果是一些大型項目的復雜業務邏輯,該怎么去實現?或者說,領域模型完成什么樣的業務邏輯?什么才是真正的業務邏輯?這個問題很重要,后續探討。
  • Application(應用層):應用層作為協調服務層,當遇到復雜性的業務邏輯時,到底如何實現,而不使其變成BLL(業務邏輯層)?認清本質很重要,后續探討。
  • 。。。

  因為時間比較緊,MessageManager 項目中很多設計或功能實現不是很合理或完善,比如:異常攔截、日志管理等都沒有實現,但走出第一步,就有第二步,第三步。。。

  如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM