关于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 项目中很多设计或功能实现不是很合理或完善,比如:异常拦截、日志管理等都没有实现,但走出第一步,就有第二步,第三步。。。
本文转自田园里的蟋蟀博客园博客,原文链接:http://www.cnblogs.com/xishuai/p/3762826.html,如需转载请自行联系原作者