天天看點

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

寫在前面

我的“第一次”,就這樣沒了: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),我們再看一下領域驅動設計的分層:

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

          來自: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(後面就這樣命名)大概類似于部落格園-短消息系統,使用者子產品暫不考慮,隻考慮短消息,大緻畫了一張功能分析圖:

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

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

  MessageManager資料分析圖:

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

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

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

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

  MessageManager的擴充圖:

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

  因為不考慮使用者子產品,是以使用者接入暫不考慮,隻擴充一個消息接口,實作方式是: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等等。

  解決方案:

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

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

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

                  來自:dax.net

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

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

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

點選檢視大圖

  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,測試主要包含倉儲持久化操作,如下:

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

  功能實作主要是領域模型設計、倉儲設計、應用層協調、表現層(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(業務邏輯層)。

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

                        點選檢視大圖

  • GitHub 開源位址:https://github.com/yuezhongxin/MessageManager
  • ASP.NET MVC 釋出位址:http://www.xishuaiblog.com:8081/
  • ASP.NET WebAPI 釋出位址:http://www.xishuaiblog.com:8082/api/Message/GetMessagesBySendUser/小菜

  注:ASP.NET WebAPI 暫隻包含:擷取發送放消息清單和擷取接收方消息清單。

  調用示例:

  • GetMessagesBySendUser(擷取發送方):http://www.xishuaiblog.com:8082/api/Message/GetMessagesBySendUser/使用者名
  • GetMessagesByReceiveUser(擷取接受方):http://www.xishuaiblog.com:8082/api/Message/GetMessagesByReceiveUser/使用者名

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

  Web 示例頁面:

撰寫短消息:

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

發件箱:

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

檢視/回複短消息:

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

  WebAPI 示例頁面:

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

  關于時間成本:

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

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

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

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

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

作者:田園裡的蟋蟀

微信公衆号:你好架構

出處:http://www.cnblogs.com/xishuai/

公衆号會不定時的分享有關架構的方方面面,包含并不局限于:Microservices(微服務)、Service Mesh(服務網格)、DDD/TDD、Spring Cloud、Dubbo、Service Fabric、Linkerd、Envoy、Istio、Conduit、Kubernetes、Docker、MacOS/Linux、Java、.NET Core/ASP.NET Core、Redis、RabbitMQ、MongoDB、GitLab、CI/CD(持續內建/持續部署)、DevOps等等。

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。

分享到:

QQ空間

新浪微網誌

騰訊微網誌

微信

更多

繼續閱讀