我的“第一次”,就这样没了: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层打交道。

  优点:

  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
项目中很多设计或功能实现不是很合理或完善,比如:异常拦截、日志管理等都没有实现,但走出第一步,就有第二步,第三步。。。

  如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^

我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践,布布扣,bubuko.com

时间: 2024-10-25 12:08:55

我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践的相关文章

(转载)浅谈我对DDD领域驱动设计的理解

原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品.所以,自然而然就想到要做一个普通电商系统,用于实现在线销售自己企业产品的目的. 再比如,我是一家互联网公司,公司有很多系统对外提供服务,面向很多客户端设备.但是最近由于各种原因,导致服务经常出故

浅谈我对DDD领域驱动设计的理解

从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品.所以,自然而然就想到要做一个普通电商系统,用于实现在线销售自己企业产品的目的. 再比如,我是一家互联网公司,公司有很多系统对外提供服务,面向很多客户端设备.但是最近由于各种原因,导致服务经常出故障.所以,我们希望通过各种措施提高服务的质量和稳定性.其中的一个措施就是希望能做一个灰度发布的平台,这个

基于DDD领域驱动设计的WCF+EF+WPF分层框架

目录置顶: 关于项目--------------------基于DDD领域驱动设计的WCF+EF+WPF分层框架(1) 架构搭建--------------------基于DDD领域驱动设计的WCF+EF+WPF分层框架(2) WCF服务端具体实现---------基于DDD领域驱动设计的WCF+EF+WPF分层框架(3) WCF客户端配置以及代理-----基于DDD领域驱动设计的WCF+EF+WPF分层框架(4) Domain具体实现------------基于DDD领域驱动设计的WCF+EF

DDD领域驱动设计初探(一):聚合

前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的东西.前面介绍了下MEF和AOP的相关知识,后面打算分享Automapper.仓储模式.WCF等东西的,可是每次准备动手写点什么的时候,就被要写的Demo难住了,比如仓储模式,使用过它的朋友应该知道,如果你的项目不是按照DDD的架构而引入仓储的设计,那么会让它变得很“鸡肋”,用不好就会十分痛苦,之前

WCF客户端配置以及代理-----基于DDD领域驱动设计的WCF+EF+WPF分层框架(4)

写在最前面:转载请注明出处 目录置顶: 关于项目--------------------基于DDD领域驱动设计的WCF+EF+WPF分层框架(1) 架构搭建--------------------基于DDD领域驱动设计的WCF+EF+WPF分层框架(2) WCF服务端具体实现---------基于DDD领域驱动设计的WCF+EF+WPF分层框架(3) WCF客户端配置以及代理-----基于DDD领域驱动设计的WCF+EF+WPF分层框架(4) Domain具体实现------------基于DD

DDD领域驱动设计之领域服务

1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 3.DDD领域驱动设计之领域基础设施层 什么是领域服务,DDD书中是说,有些类或者方法,放实体A也不好,放实体B也不好,因为很可能会涉及多个实体或者聚合的交互(也可能是多个相同类型的实体),此时就应该吧这些代码放到领域服务中,领域服务其实就跟传统三层的BLL很相似,只有方法没有属性,也就没有状态,而且最好是用动词命名,service为后缀,但是真正到了实践的时候,很多时候是很难区分是领域实体本身实现还是用领域

DDD领域驱动设计之聚合、实体、值对象

关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真正项目实践过,甚至也没有看过真正的DDD实践的项目源码,处于极度纠结状态,甚至无法自拔,所以告诫DDD爱好者们,如果要在项目里面实践DDD,除非你对实体建模和领域职责非常了解(很多时候会纠结一些逻辑放哪里好,属于设计问题)以及你的团队水平都比较高认同DDD,否则请慎重...勿喷! 代码在后,请先看D

DDD领域驱动设计之领域基础设施层

1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 其实这里说的基础设施层只是领域层的一些接口和基类而已,没有其他的如日子工具等代码,仅仅是为了说明领域层的一些基础问题 1.领域事件简单实现代码,都是来至ASP.NET设计模式书中的代码 namespace DDD.Infrastructure.Domain.Events { public interface IDomainEvent { } } namespace DDD.Infrastructure.Dom

DDD领域驱动设计仓储Repository

DDD领域驱动设计初探(二):仓储Repository(上) 前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原因无非以下两点:一是Repository的真实意图没有理解清楚,导致设计的紊乱,随着项目的横向和纵向扩展,到最后越来越难维护:二是赶时髦的为了“模式”而“模式”,仓储并非适用于所有项目,这就像没有任何一种架构能解决所有的设计难题一样.本篇

DDD领域驱动设计实践篇之如何提取模型

需求说明: 省级用户可以登记国家指标 省级用户和市级用户可以登记指标分解 登记国家指标时,需要录入以下数据:指标批次.文号.面积,这里省略其他数据,下同 登记指标分解时,需要录入以下数据:指标批次.文号.面积,以及可以选择多个市(市级登记的时候是县)的指标,每个市(县)的指标也是要输入批次.文号.面积 登记指标分解时,一个指标批次不能选择多个相同的市(县) 登记指标分解时,需要判断当前剩余面积是否足够,比如省登记的时候,要看国家本年度下发给省的指标面积是否大于省本年度所以指标面积,登记国家指标不