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),我们再看一下领域驱动设计的分层:
来自: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 项目中很多设计或功能实现不是很合理或完善,比如:异常拦截、日志管理等都没有实现,但走出第一步,就有第二步,第三步。。。
如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^
DDD(领域驱动设计)理论结合实践的更多相关文章
- DDD领域驱动设计理论篇 - 学习笔记
一.Why DDD? 在加入X公司后,开始了ASP.NET Core+Docker+Linux的技术实践,也开始了微服务架构的实践.在微服务的学习中,有一本微软官方出品的<.NET微服务:容器化 ...
- 关于DDD领域驱动设计的理论知识收集汇总
原文:关于DDD领域驱动设计的理论知识收集汇总 最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下: 我一直觉得不要盲目相信权威,比如不能一 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(3)
上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(2)> 这篇文章主要是对 DDD.Sample 框架增加 Transa ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(2)
上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(1)> 阅读目录: 抽离 IRepository 并改造 Reposi ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(1)
好久没写 DDD 领域驱动设计相关的文章了,嘎嘎!!! 这几天在开发一个新的项目,虽然不是基于领域驱动设计的,但我想把 DDD 架构设计的一些东西运用在上面,但发现了很多问题,这些在之前的短消息项目中 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(转)
http://www.cnblogs.com/xishuai/p/ddd-repository-iunitofwork-and-idbcontext.html 好久没写 DDD 领域驱动设计相关的文章 ...
- DDD领域驱动设计和实践(转载)
-->目录导航 一. DDD领域驱动设计介绍 1. 什么是领域驱动设计(DDD) 2. 领域驱动设计的特点 3. 如果不使用DDD? 4. 领域驱动设计的分层架构和构成要素 5. 事务脚本和领域 ...
- DDD领域驱动设计落地实践(十分钟看完,半小时落地)
一.引子 不知今年吹了什么风,忽然DDD领域驱动设计进入大家视野.该思想源于2003年 Eric Evans编写的"Domain-Driven Design领域驱动设计"简称DDD ...
- DDD领域驱动设计之聚合、实体、值对象
关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真 ...
- DDD领域驱动设计的理解
DDD领域驱动设计的理解 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能 ...
随机推荐
- ACM-凸多边形的计算几何——hrbust1429
凸多边形 称号:http://acm.hrbust.edu.cn/index.php?m=ProblemSet&a=showProblem&problem_id=1429 Descri ...
- mariadb 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
[root@localhost /]# systemctl stop mariadb.service[root@localhost /]# mysqld_safe --user=mysql --ski ...
- sql小总结
---------------------------------------------------------------------------------------------------- ...
- string.Format对C#字符串格式化
String.Format 方法的几种定义: String.Format (String, Object) 将指定的 String 中的格式项替换为指定的 Object 实例的值的文本等效项.Stri ...
- android进度条
android进度条 1.达到的效果 2.布局代码 先写一个my_browser.xml文件 存放WebView <?xml version="1.0" encoding=& ...
- 乐趣与你rabbitMQ 源代码
RabbitMQ API RabbitMQ Server它提供了丰富的http api. 对于列子 须要HTTP基本身份验证.默认的username/password为guest/guest. 这些返 ...
- 用python+selenium导入excel文件
连接mysql #encoding=utf-8 import pymysql import time class ConnMysql(object): def __init__(self): self ...
- 收集的css布局
1 <title>左定宽,右自动</title> 2 <style> 3 body{margin:0px;padding:0px;} 4 .box .left,.b ...
- jquery 调用wcf 的SOA架构,将三层架构运用到SOA的架构中来(第四天)
经过前面3天的学习,我想大家应该对SOA的架构有了初步的了解,其实 SOA与三层架构并不冲突,而是三层架构的升级版. 来看下传统的三层架构! 一共可以分为4个层: 模型层(可有可无),客户端,服务端, ...
- 详细的图文介绍如何利用XAMPP本地建站的环境配置教程
原文:详细的图文介绍如何利用XAMPP本地建站的环境配置教程 WordPress 是一个简便快捷,用途广,人气旺的一个开源的博客建站程序.很有很多等您去发现. 简便快捷:在性能上易于操作.易于浏览: ...