寫在前面
插一句:本人超愛落網-《平凡的世界》這一期,分享給大家。
閱讀目錄:
第一次聽你,清風吹送,田野短笛;第一次看你,半彎新湖,魚躍翠堤;第一次念你,燕飛巢冷,釋懷記憶;第一次夢你,雲翔海島,輪渡迤邐;第一次認你,怨江別續,草橋知己;第一次怕你,命懸一線,遺憾禁忌;第一次悟你,千年菩提,生死一起。
人生有很多的第一次:小時候第一次牙牙學語、第一次學蹣跚學步。。。長大后第一次上課、第一次逃課、第一次騎自行車、第一次懂事、第一次和喜歡的人說“我愛你”、第一次旅行、第一次敞開心扉去認識這個世界。。。
第一次的感覺:有甜蜜、有辛酸;有勇敢、有羞澀;有成功、有失敗。不管怎樣,都要勇敢的邁出第一步,不論成功與失敗,至少自己努力過,證明過自己就好,就像哥倫布探索美洲一樣,沒有勇敢邁出第一步,也許現在“美洲”的概念會推遲不知多少年。
以下內容,只是一些個人看法和實現,僅供參考學習,也歡迎討論指教。
關於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層打交道。
優點:
- 更加符合OO的原則。
- Service層很薄,只充當Facade的角色,不和Dao打交道。
缺點:
- Dao和Domain Object形成了雙向依賴,復雜的雙向依賴會導致很多潛在的問題。
- 如何划分Service層邏輯和Domain層邏輯是非常含混的,在實際項目中,由於設計和開發人員的水平差異,可能導致整個結構的混亂無序。 (這個問題在項目實際運作的時候會出現,划分很重要。)
- 考慮到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(業務邏輯層)。
開源-發布
- 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 示例頁面:
撰寫短消息:
發件箱:
查看/回復短消息:
WebAPI 示例頁面:
后記
關於時間成本:
- MessageManager項目:兩天(包含晚上)+兩個晚上;
- 本篇博客:一個下午+一個晚上(很晚)+外加更正無數;
關於DDD實踐-MessageManager項目,有幾個問題需要記錄一下:
- Domain Model(領域模型):領域模型到底該怎么設計?你會看到,MessageManager項目中的User和Message領域模型是非常貧血的,沒有包含任何的業務邏輯,現在網上很多關於DDD示例項目多數也存在這種情況,當然項目本身沒有業務,只是簡單的“CURD”操作,但是如果是一些大型項目的復雜業務邏輯,該怎么去實現?或者說,領域模型完成什么樣的業務邏輯?什么才是真正的業務邏輯?這個問題很重要,后續探討。
- Application(應用層):應用層作為協調服務層,當遇到復雜性的業務邏輯時,到底如何實現,而不使其變成BLL(業務邏輯層)?認清本質很重要,后續探討。
- 。。。
因為時間比較緊,MessageManager 項目中很多設計或功能實現不是很合理或完善,比如:異常攔截、日志管理等都沒有實現,但走出第一步,就有第二步,第三步。。。
如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^