拨开迷雾,找回自我:DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计?

写在前面

  阅读目录:

  • 迷雾森林
  • 找回自我
  • 开源地址
  • 后记

  毫无疑问,领域驱动设计的核心是领域模型,领域模型的核心是实现业务逻辑,也就是说,在应对具体的业务场景的时候,实现业务逻辑是领域驱动设计最重要的一环,在写这篇博文之前,先总结下之前关于 DDD(领域驱动设计)的三篇博文:

  OO(面向对象)设计指导我们要面向(停顿一下)对象设计,也就是说你要去面向这个对象,去思考它的本质,说白了就是思考:它是从哪里来,到哪里去,生长轨迹,拥有的事物,自身的变化规律,现在的状态等等方面,更深入一点就是站在哲学的角度去思考万物的本质(请参考老子的道德经),咳咳,说大了。

  但是,在现实生活中,我们是做软件设计的程序员,需要做出东西给用户用的,而不是像老子那样逍遥的每天面对星空,去思考宇宙万物(如果有兴趣,你也可以试试,只要不被你的女朋友骂死)。也就是说我们要面对具体的业务场景,而并非只是单纯的 OO,这就要求我们除了要 OO 之外,还要去探讨业务逻辑的本质以及实现。

  以下内容是本人掉的一个又一个深坑,拼死爬了上来,在 DDD 的道路上,友情提醒各位:前方有坑,园友们请小心谨慎。

迷雾森林

  迷雾森林中,切勿迷失自我,不幸的是,我就这样迷失了:

  具体的业务场景还是短消息系统-MessageManager,存在 Message 和 User 两个领域模型,业务逻辑:一个用户给另一个用户发送消息,就是这么简单,可以看作是一个最简单的业务逻辑,当然在发送消息这个过程中会有其他的业务逻辑,先不探讨 Message 领域模型和 User 领域模型如何协调完成这个业务逻辑,我们先看以下,我在第一篇《我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践》博文中,关于领域模型的实现:

 1 /**
 2 * author:xishaui
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5
 6 using System;
 7
 8 namespace MessageManager.Domain.DomainModel
 9 {
10     public class Message : IAggregateRoot
11     {
12         #region 构造方法
13         public Message()
14         {
15             this.ID = Guid.NewGuid().ToString();
16         }
17         #endregion
18
19         #region 实体成员
20         public string FromUserID { get; set; }
21         public string FromUserName { get; set; }
22         public string ToUserID { get; set; }
23         public string ToUserName { get; set; }
24         public string Title { get; set; }
25         public string Content { get; set; }
26         public DateTime SendTime { get; set; }
27         public bool IsRead { get; set; }
28         public virtual User FromUser { get; set; }
29         public virtual User ToUser { get; set; }
30         #endregion
31
32         #region IEntity成员
33         /// <summary>
34         /// 获取或设置当前实体对象的全局唯一标识。
35         /// </summary>
36         public string ID { get; set; }
37         #endregion
38     }
39 }

  Are you kidding me?不,你没看错,以上就是 Message 领域模型的实现代码,User 领域模型的代码我就不贴了,比这个还要简单,只包含 ID 和 Name 两个字段属性,领域驱动设计主张的是充血模型,只包含字段属性的领域模型是极其贫血的,像上面的 Message 领域模型,充血的领域模型实现的是业务逻辑。上面我们说的发送消息这个业务逻辑,在领域模型中为什么没有体现?既然是基于领域驱动设计,那为什么我还要这样设计呢?这是为什么呢?当时设计完之后,我也在思考这个问题,难道脑袋有问题?不可能吧?看下应用层的代码就知道了:

  1 /**
  2 * author:xishuai
  3 * address:https://www.github.com/yuezhongxin/MessageManager
  4 **/
  5
  6 using AutoMapper;
  7 using MessageManager.Application.DTO;
  8 using MessageManager.Domain.DomainModel;
  9 using MessageManager.Domain.Repositories;
 10 using System.Collections.Generic;
 11
 12 namespace MessageManager.Application.Implementation
 13 {
 14     /// <summary>
 15     /// Message管理应用层接口实现
 16     /// </summary>
 17     public class MessageServiceImpl : ApplicationService, IMessageService
 18     {
 19         #region Private Fields
 20         private readonly IMessageRepository messageRepository;
 21         private readonly IUserRepository userRepository;
 22         #endregion
 23
 24         #region Ctor
 25         /// <summary>
 26         /// 初始化一个<c>MessageServiceImpl</c>类型的实例。
 27         /// </summary>
 28         /// <param name="context">用来初始化<c>MessageServiceImpl</c>类型的仓储上下文实例。</param>
 29         /// <param name="messageRepository">“消息”仓储实例。</param>
 30         /// <param name="userRepository">“用户”仓储实例。</param>
 31         public MessageServiceImpl(IRepositoryContext context,
 32             IMessageRepository messageRepository,
 33             IUserRepository userRepository)
 34             : base(context)
 35         {
 36             this.messageRepository = messageRepository;
 37             this.userRepository = userRepository;
 38         }
 39         #endregion
 40
 41         #region IMessageService Members
 42         /// <summary>
 43         /// 通过发送方获取消息列表
 44         /// </summary>
 45         /// <param name="userDTO">发送方</param>
 46         /// <returns>消息列表</returns>
 47         public IEnumerable<MessageDTO> GetMessagesBySendUser(UserDTO sendUserDTO)
 48         {
 49             //User user = userRepository.GetUserByName(sendUserDTO.Name);
 50             var messages = messageRepository.GetMessagesBySendUser(Mapper.Map<UserDTO, User>(sendUserDTO));
 51             if (messages == null)
 52                 return null;
 53             var ret = new List<MessageDTO>();
 54             foreach (var message in messages)
 55             {
 56                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 57             }
 58             return ret;
 59         }
 60         /// <summary>
 61         /// 通过接受方获取消息列表
 62         /// </summary>
 63         /// <param name="userDTO">接受方</param>
 64         /// <returns>消息列表</returns>
 65         public IEnumerable<MessageDTO> GetMessagesByReceiveUser(UserDTO receiveUserDTO)
 66         {
 67             //User user = userRepository.GetUserByName(receiveUserDTO.Name);
 68             var messages = messageRepository.GetMessagesByReceiveUser(Mapper.Map<UserDTO, User>(receiveUserDTO));
 69             if (messages == null)
 70                 return null;
 71             var ret = new List<MessageDTO>();
 72             foreach (var message in messages)
 73             {
 74                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 75             }
 76             return ret;
 77         }
 78         /// <summary>
 79         /// 删除消息
 80         /// </summary>
 81         /// <param name="messageDTO"></param>
 82         /// <returns></returns>
 83         public bool DeleteMessage(MessageDTO messageDTO)
 84         {
 85             messageRepository.Remove(Mapper.Map<MessageDTO, Message>(messageDTO));
 86             return messageRepository.Context.Commit();
 87         }
 88         /// <summary>
 89         /// 发送消息
 90         /// </summary>
 91         /// <param name="messageDTO"></param>
 92         /// <returns></returns>
 93         public bool SendMessage(MessageDTO messageDTO)
 94         {
 95             Message message = Mapper.Map<MessageDTO, Message>(messageDTO);
 96             message.FromUserID = userRepository.GetUserByName(messageDTO.FromUserName).ID;
 97             message.ToUserID = userRepository.GetUserByName(messageDTO.ToUserName).ID;
 98             messageRepository.Add(message);
 99             return messageRepository.Context.Commit();
100         }
101         /// <summary>
102         /// 查看消息
103         /// </summary>
104         /// <param name="ID"></param>
105         /// <returns></returns>
106         public MessageDTO ShowMessage(string ID, string isRead)
107         {
108             Message message = messageRepository.GetByKey(ID);
109             if (isRead == "1")
110             {
111                 message.IsRead = true;
112                 messageRepository.Update(message);
113                 messageRepository.Context.Commit();
114             }
115             return Mapper.Map<Message, MessageDTO>(message);
116         }
117         #endregion
118     }
119 }

  可以看到应用层的代码真是不忍直视,撇开其他操作,我们看下 SendMessage 这个方法,首先 MessageDTO 这个参数就不应该存在,下面用 AutoMapper 进行对象转化,然后再进行赋值操作,这个过程就是典型的过程思维模式,没有体现出一点的 OO 思想,赋值完之后,使用 Repository(仓储)进行持久话,发送消息的这个业务逻辑体现在哪?如果硬要说体现的话,那就是:messageRepository.Add(message) 这段代码了,想想当时无知的认为,发送消息的业务逻辑体现就是持久化数据库,还真是可笑。

  在这篇博文发表后,很多园友也都意识到了这个问题,什么问题?主要是以下两个:

  • Domain Model(领域模型):领域模型到底该怎么设计?你会看到,MessageManager 项目中的 User 和 Message 领域模型是非常贫血的,没有包含任何的业务逻辑,现在网上很多关于 DDD 示例项目多数也存在这种情况,当然项目本身没有业务,只是简单的“CURD”操作,但是如果是一些大型项目的复杂业务逻辑,该怎么去实现?或者说,领域模 型完成什么样的业务逻辑?什么才是真正的业务逻辑?这个问题很重要,后续探讨。
  • Application(应用层):应用层作为协调服务层,当遇到复杂性的业务逻辑时,到底如何实现,而不使其变成 BLL(业务逻辑层)?认清本质很重要,后续探讨。

  简而言之就是:领域模型太贫血;应用层变成了业务逻辑层。意识到问题,那就找问题所在,经过一番探查,把 Repository 作为了重点怀疑对象,为什么?主要是我当时以为 Repository 的职责有问题,也就有了下面的这篇博文《一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?》,在这篇博文中,关于上面原因的分析,主要讲到了以下两个节点的内容:

  虽然博文中也讲到了领域模型的重新设计,但是设计之后还是一坨屎,这边就不拿出来误导大家了。回到上面的问题,关于 Repository 的职责问题,我当时是这样分析的:

Repository 应用在应用层,这样就致使应用层和基础层(我把数据持久化放在基础层了)通信,忽略了最重要的领域层,领域层在其中起到的作用最多也就是传递一个非常贫血的领域模型,然后通过 Repository 进行“CRUD”,这样的结果是,应用层不变成所谓的 BLL(常说的业务逻辑层)才怪,另外,因为业务逻辑都放在应用层了,领域模型也变得更加贫血。

  乍一看,上面的分析还真没什么问题(看来我还是蛮会忽悠人的,嘿嘿),Repository 服务于领域,所以就必须把 Repository 的调用放在领域层中,领域模型又不能直接和 Repository 通信,所以我后来就把 Domain Service(领域服务)加了进来,让领域服务和 Repository 进行协调,然后应用层和就和领域服务通信了,然后的然后。。。

  有朋友看到这,会觉得没错啊,就是这样啊(如果你也这样认为,那我就去干传销了),先不讨论对错,我们看下领域服务究竟实现的是个什么东西?领域模型变成了什么?应用层又变成了什么?

  领域服务代码:

 1 /**
 2 * author:xishuai
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5
 6 using MessageManager.Domain.DomainModel;
 7 using MessageManager.Domain.Repositories;
 8 using System.Collections.Generic;
 9
10 namespace MessageManager.Domain.DomainService
11 {
12     /// <summary>
13     /// Message领域服务实现
14     /// </summary>
15     public class MessageDomainService : IMessageDomainService
16     {
17         #region Private Fields
18         private readonly IMessageRepository messageRepository;
19         private readonly IUserRepository userRepository;
20         #endregion
21
22         #region Ctor
23         public MessageDomainService(IMessageRepository messageRepository, IUserRepository userRepository)
24         {
25             this.messageRepository = messageRepository;
26             this.userRepository = userRepository;
27         }
28         #endregion
29
30         #region IMessageDomainService Members
31         public bool DeleteMessage(Message message)
32         {
33             messageRepository.Remove(message);
34             return messageRepository.Context.Commit();
35         }
36         public bool SendMessage(Message message)
37         {
38             message.LoadUserName(userRepository.GetUser(new User { Name = message.FromUserName })
39                 , userRepository.GetUser(new User { Name = message.ToUserName }));
40             messageRepository.Add(message);
41             return messageRepository.Context.Commit();
42         }
43         public Message ShowMessage(string id, User currentUser)
44         {
45             Message message = messageRepository.GetByKey(id);
46             message.ReadMessage(userRepository.GetUser(new User { Name = currentUser.Name }));
47             messageRepository.Update(message);
48             messageRepository.Context.Commit();
49             return message;
50         }
51         public IEnumerable<Message> GetMessagesBySendUser(User user)
52         {
53             User userResult = userRepository.GetUser(user);
54             return messageRepository.GetMessagesBySendUser(userResult);
55         }
56         public IEnumerable<Message> GetMessagesByReceiveUser(User user)
57         {
58             User userResult = userRepository.GetUser(user);
59             return messageRepository.GetMessagesByReceiveUser(userResult);
60         }
61         public int GetNoReadCount(User user)
62         {
63             User userResult = userRepository.GetUser(user);
64             return messageRepository.GetNoReadCount(userResult);
65         }
66         #endregion
67     }
68 }

  应用层代码:

  1 /**
  2 * author:xishuai
  3 * address:https://www.github.com/yuezhongxin/MessageManager
  4 **/
  5
  6 using AutoMapper;
  7 using MessageManager.Application.DTO;
  8 using MessageManager.Domain.DomainModel;
  9 using MessageManager.Domain.DomainService;
 10 using System.Collections.Generic;
 11
 12 namespace MessageManager.Application.Implementation
 13 {
 14     /// <summary>
 15     /// Message管理应用层接口实现
 16     /// </summary>
 17     public class MessageServiceImpl : ApplicationService, IMessageService
 18     {
 19         #region Private Fields
 20         private readonly IMessageDomainService messageService;
 21         #endregion
 22
 23         #region Ctor
 24         /// <summary>
 25         /// 初始化一个<c>MessageServiceImpl</c>类型的实例。
 26         /// </summary>
 27         /// <param name="messageRepository">“消息”服务实例。</param>
 28         public MessageServiceImpl(IMessageDomainService messageService)
 29         {
 30             this.messageService = messageService;
 31         }
 32         #endregion
 33
 34         #region IMessageService Members
 35         /// <summary>
 36         /// 通过发送方获取消息列表
 37         /// </summary>
 38         /// <param name="userDTO">发送方</param>
 39         /// <returns>消息列表</returns>
 40         public IEnumerable<MessageDTO> GetMessagesBySendUser(UserDTO sendUserDTO)
 41         {
 42             //User user = userRepository.GetUserByName(sendUserDTO.Name);
 43             var messages = messageService.GetMessagesBySendUser(Mapper.Map<UserDTO, User>(sendUserDTO));
 44             if (messages == null)
 45                 return null;
 46             var ret = new List<MessageDTO>();
 47             foreach (var message in messages)
 48             {
 49                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 50             }
 51             return ret;
 52         }
 53         /// <summary>
 54         /// 通过接受方获取消息列表
 55         /// </summary>
 56         /// <param name="userDTO">接受方</param>
 57         /// <returns>消息列表</returns>
 58         public IEnumerable<MessageDTO> GetMessagesByReceiveUser(UserDTO receiveUserDTO)
 59         {
 60             //User user = userRepository.GetUserByName(receiveUserDTO.Name);
 61             var messages = messageService.GetMessagesByReceiveUser(Mapper.Map<UserDTO, User>(receiveUserDTO));
 62             if (messages == null)
 63                 return null;
 64             var ret = new List<MessageDTO>();
 65             foreach (var message in messages)
 66             {
 67                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 68             }
 69             return ret;
 70         }
 71         /// <summary>
 72         /// 删除消息
 73         /// </summary>
 74         /// <param name="messageDTO"></param>
 75         /// <returns></returns>
 76         public bool DeleteMessage(MessageDTO messageDTO)
 77         {
 78             return messageService.DeleteMessage(Mapper.Map<MessageDTO, Message>(messageDTO));
 79         }
 80         /// <summary>
 81         /// 发送消息
 82         /// </summary>
 83         /// <param name="messageDTO"></param>
 84         /// <returns></returns>
 85         public bool SendMessage(MessageDTO messageDTO)
 86         {
 87             return messageService.SendMessage(Mapper.Map<MessageDTO, Message>(messageDTO));
 88         }
 89         /// <summary>
 90         /// 查看消息
 91         /// </summary>
 92         /// <param name="ID"></param>
 93         /// <returns></returns>
 94         public MessageDTO ShowMessage(string id, UserDTO currentUserDTO)
 95         {
 96             Message message = messageService.ShowMessage(id, Mapper.Map<UserDTO, User>(currentUserDTO));
 97             return Mapper.Map<Message, MessageDTO>(message);
 98         }
 99         /// <summary>
100         /// 获取未读消息数
101         /// </summary>
102         /// <param name="user"></param>
103         /// <returns></returns>
104         public int GetNoReadCount(UserDTO userDTO)
105         {
106             return messageService.GetNoReadCount(Mapper.Map<UserDTO, User>(userDTO));
107         }
108         #endregion
109     }
110 }

  领域模型代码:

 1 /**
 2 * author:xishaui
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5
 6 using System;
 7
 8 namespace MessageManager.Domain.DomainModel
 9 {
10     public class Message : IAggregateRoot
11     {
12         #region 构造方法
13         public Message()
14         {
15             this.ID = Guid.NewGuid().ToString();
16         }
17         #endregion
18
19         #region 实体成员
20         public string FromUserID { get; set; }
21         public string FromUserName { get; set; }
22         public string ToUserID { get; set; }
23         public string ToUserName { get; set; }
24         public string Title { get; set; }
25         public string Content { get; set; }
26         public DateTime SendTime { get; set; }
27         public bool IsRead { get; set; }
28         public virtual User FromUser { get; set; }
29         public virtual User ToUser { get; set; }
30         #endregion
31
32         #region 业务逻辑
33         /// <summary>
34         /// 阅读消息
35         /// </summary>
36         /// <param name="CurrentUser"></param>
37         public void ReadMessage(User currentUser)
38         {
39             if (!this.IsRead && currentUser.ID.Equals(ToUserID))
40             {
41                 this.IsRead = true;
42             }
43         }
44         /// <summary>
45         /// 加载用户
46         /// </summary>
47         /// <param name="sendUser"></param>
48         /// <param name="receiveUser"></param>
49         public void LoadUserName(User sendUser, User receiveUser)
50         {
51             this.FromUserID = sendUser.ID;
52             this.ToUserID = receiveUser.ID;
53         }
54         #endregion
55
56         #region IEntity成员
57         /// <summary>
58         /// 获取或设置当前实体对象的全局唯一标识。
59         /// </summary>
60         public string ID { get; set; }
61         #endregion
62     }
63 }

  其实上面代码,如果是 DDD 大神来看的话,他只要看领域模型中的代码就行了,因为领域驱动设计的核心就是领域模型,那领域模型变成了什么?只是添加了 ReadMessage 和 LoadUserName 两个不是业务逻辑的业务逻辑方法(因为只有他们两个,如果把他们两个去掉,就变回原来的贫血模型了,所以,你懂的),领域服务中的 SendMessage 方法变的和原来的应用层代码一样,要说变化的话,只是把 Application 单词变成了 Domain Service 这个单词,其他无任何变化,应用层的代码也就变成了下面这样:

1         /// <summary>
2         /// 发送消息
3         /// </summary>
4         /// <param name="messageDTO"></param>
5         /// <returns></returns>
6         public bool SendMessage(MessageDTO messageDTO)
7         {
8             return messageService.SendMessage(Mapper.Map<MessageDTO, Message>(messageDTO));
9         }

  在领域驱动设计中,应用层的定义是很薄的一层,可以看到,上面的应用层代码也未免太薄了吧,为什么?因为原来它的工作让领域服务做了,导致现在变成了一个调用外壳(也就是可有可无的东西,没有任何意义)。

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》分割线《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《

  为什么会有分割线?因为在应对具体业务场景中,上面做的操作都是无用功,为什么?因为设计的领域模型中什么东西都没有(指的是业务逻辑),没有任何东西的领域模型,还是真正的领域驱动设计吗?关于这个问题,傻子都知道,当然我也不傻,嘿嘿。

  认识到这个根本问题后,下面就抛开一切外在因素,比如领域服务、仓储、应用层、表现层等等,这些统统不管,只做领域模型的设计,让真正的设计焦点集中在领域模型上,然后再针对领域模型做单元测试。

  对,就是这么简单,至少听起来的确简单,事实真是这样吗?我却不这样认为,因为就是这个简单的问题,我为此痛苦了两三天的时间,说夸张点就是:吃不下饭,睡不着觉,在这个过程中,领域模型的一行代码我也没有写,不是不想写,而是不知道如何写?这是最最痛苦的,真是体会到了才知道。写不出来怎么办?我就找遍网上所有关于领域模型设计的资料(大部分都是英文,只能很痛苦的看),还有《领域驱动设计》和《企业应用架构模式》这两本书,希望能从中找到些灵感(就像画画,难点就在如何画第一笔),并不是模仿,这个也模仿不来,因为每个业务场景都不相同,遗憾的是没有找到任何的灵感,唯一找到的线索就是 OO 设计(大家都知道,我却不知道,因为蒙了)。

  既然是要面向对象,那就分析一下对象,主要包含两个:用户和消息。对象拥有自身的属性、状态和行为,发送消息是用户的一种行为,所以发送消息这个操作应该放在用户中,那现在消息只有自身的一些属性值,因为在面向对象中,它是固定的,只有通过用户来调用它,用户领域模型代码如下:

 1 /**
 2 * author:xishaui
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5
 6 using MessageManager.Domain.Demo.V1.Event;
 7 using System;
 8 using System.Collections.Generic;
 9
10 namespace MessageManager.Domain.Demo.V1
11 {
12     public class User
13     {
14         public User()
15         {
16             this.ID = Guid.NewGuid().ToString();
17         }
18
19         public string ID { get; set; }
20         public string Name { get; set; }
21         public virtual ICollection<Message> SendMessages { get; set; }
22         public virtual ICollection<Message> ReceiveMessages { get; set; }
23
24         public void SendMessage(Message message)
25         {
26             User toUser = GetUser(message.ToUser);
27             if (toUser == null)
28             {
29                 throw new NotImplementedException();
30             }
31             message.FromUser = this;
32             message.ToUser = toUser;
33             this.SendMessages.Add(message);
34             toUser.ReceiveMessages.Add(message);
35             DomainEvents.Raise(new MessageEvent() { DoMessage = message });
36             ///
37         }
38     }
39 }

  按照面向对象设计,消息是用户的附属对象,只有用户存在,消息才有意义,一个用户对象拥有多个消息的对象集合,那怎么体现出发送消息这个动作呢?答案就是:this.SendMessages.Add(message) 这段代码,表示往用户对象的消息集合填充消息对象,这样就会相对于用户对象来说,这条消息的发送动作就完成了。先不考虑这样设计的合理或者不合理,我们看下消息模型中的代码,就会发现里面只有一些字段属性,没有任何的操作,还有就是如果我们要添加消息的其他动作,比如查找,删除等等,按照上面的分析,我们就会在用户对象中添加这些操作,因为这些动作都是用户所具有的,合理吗?至少听起来就不合理。

  身处这个迷雾森林,才知道它的恐怖之处,不断的迷失自我,以致最后可能连自己都不相信,并怀疑自己。

找回自我

  在迷雾森林之中,如何找回自我?而不迷失,没有确切的答案,我只能寻觅那一缕阳光一步一步的往前行。。。

  首先,Repository,和你说声抱歉,非常抱歉,让你蒙冤,是我误会你了,因为我对业务逻辑的不理解,以致做出错误的做法。

  回到短消息系统-MessageManager,需要注意的是,我们做的是消息系统,一切的一切都应该围绕 Message 领域模型展开,在这个系统中,最重要的就是发送消息这个业务逻辑,什么叫发消息?不要被上面的面向对象所迷惑,只考虑发消息这个具体的业务,我们来分析一下:比如在现实生活中,我们要给女朋友写信,首先我们要写信的内容,写完之后,要写一下女朋友的地址信息及名字,这个写信才算完成,邮递员邮递并不在这个业务逻辑之内了,因为这封信我写上收件人之后,这封信相对于我来说就已经发出了,后面只不过是收件人收不收得到的问题了(即使我写好,没有寄出去)。也就是说邮递员邮递这个工作过程相当于数据的持久化,写信的这个过程就是邮递(发消息的业务逻辑),just it。

  理解上面的内容很重要,然后我们再来看 Message 这个领域模型,创建这个对象的时候,就说明我们已经把消息的内容写好了,也就是必要的东西,比如:消息标题、消息内容、发送人、发送时间等等,用代码实现就是在 Message 领域模型中的构造函数传递必要值,代码如下:

 1 /**
 2 * author:xishuai
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5
 6 using System;
 7
 8 namespace MessageManager.Domain.DomainModel
 9 {
10     public class Message : IAggregateRoot
11     {
12         public Message(string title, string content, User sendUser)
13         {
14             if (title.Equals("") || content.Equals("") || sendUser == null)
15             {
16                 throw new ArgumentNullException();
17             }
18             this.ID = Guid.NewGuid().ToString();
19             this.Title = title;
20             this.Content = content;
21             this.SendTime = DateTime.Now;
22             this.State = MessageState.NoRead;
23             this.SendUser = sendUser;
24         }
25         public string ID { get; set; }
26         public string Title { get; set; }
27         public string Content { get; set; }
28         public DateTime SendTime { get; set; }
29         public MessageState State { get; set; }
30         public virtual User SendUser { get; set; }
31         public virtual User ReceiveUser { get; set; }
32
33         public bool Send(User receiveUser)
34         {
35             if (receiveUser == null)
36             {
37                 throw new ArgumentNullException();
38             }
39             this.ReceiveUser = receiveUser;
40             return true;
41             ///to do...
42         }
43     }
44 }

  在实例 Message 领域模型之前要对必要值进行判断,发送消息的关键代码就是:ReceiveUser = receiveUser,表示为这条消息“贴上”收件人的标签,指示这条消息的发送动作已经完成,当然在这个 Send 业务方法中可能还会有其他业务逻辑的加入,其实发送消息就是这样,在 Message 这个领域模型中,没有什么数据库的概念,只是描述这个业务功能,仅此而已。

  在领域驱动设计的过程中,你会忘记数据库的存在,使用接口注入,我们可以想怎么操作就怎么操作,数据库只是业务场景中数据的存储的一种方式,这个工作应该是你做完所有的业务设计之后执行,如果想进行单元测试,使用 IRepository 接口,我们甚至可以虚拟一切想要的对象(是对象,不是数据值)。

  我们再来看下应用层的实现:

 1 /**
 2 * author:xishuai
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5
 6 using MessageManager.Domain.DomainModel;
 7 using MessageManager.Domain.Repositories;
 8
 9 namespace MessageManager.Application.Implementation
10 {
11     /// <summary>
12     /// Message管理应用层接口实现
13     /// </summary>
14     public class MessageServiceImpl : ApplicationService, IMessageService
15     {
16         #region Private Fields
17         private readonly IMessageRepository messageRepository;
18         private readonly IUserRepository userRepository;
19         #endregion
20
21         #region Ctor
22         /// <summary>
23         /// 初始化一个<c>MessageServiceImpl</c>类型的实例。
24         /// </summary>
25         /// <param name="context">用来初始化<c>MessageServiceImpl</c>类型的仓储上下文实例。</param>
26         /// <param name="messageRepository">“消息”仓储实例。</param>
27         /// <param name="userRepository">“用户”仓储实例。</param>
28         public MessageServiceImpl(IRepositoryContext context,
29             IMessageRepository messageRepository,
30             IUserRepository userRepository)
31             : base(context)
32         {
33             this.messageRepository = messageRepository;
34             this.userRepository = userRepository;
35         }
36         #endregion
37
38         #region IMessageService Members
39         /// <summary>
40         /// 发送消息
41         /// </summary>
42         /// <param name="title">消息标题</param>
43         /// <param name="content">消息内容</param>
44         /// <param name="sendLoginUserName">发送人-登陆名</param>
45         /// <param name="receiveDisplayUserName">接受人-接收人</param>
46         /// <returns></returns>
47         public bool SendMessage(string title, string content, string sendLoginUserName, string receiveDisplayUserName)
48         {
49             User sendUser = userRepository.GetUserByLoginName(sendLoginUserName);
50             if (sendUser == null)
51             {
52                 return false;
53             }
54             Message message = new Message(title, content, sendUser);
55             User receiveUser = userRepository.GetUserByDisplayName(receiveDisplayUserName);
56             if (receiveUser == null)
57             {
58                 return false;
59             }
60             if (message.Send(receiveUser))
61             {
62                 return true;
63                 //messageRepository.Add(message);
64                 //return messageRepository.Context.Commit();
65             }
66             else
67             {
68                 return false;
69             }
70         }
71         #endregion
72     }
73 }

  你会发现应用层中的 SendMessage 方法,所做的工作流程是多么的行云流水,一步一步的协调(不要误读为业务逻辑,我之前就是这样),完美的完成这个发送消息的请求。应用层的核心是什么?答案就是协调,什么意思?就是说 UI 发出一个请求(比如发消息),然后应用层接到这这个请求之后,进行一些协调处理(比如取想要的用户值,完成发送,以及发送之后的流程-发邮件等等),完成这个工作流程,而并不是这个业务逻辑,业务逻辑是发消息。

  在代码注释的地方,完成的是消息的持久化操作,当然我们也可以不完成这个操作,因为业务逻辑是业务逻辑,持久化是持久化,并没有半毛钱关系,我们描述的只是业务场景,仅此而已。

开源地址

后记

  以上只是一个简单业务场景用例,就让我在迷雾森林中迷失自我这么久,到现在只是看到了那一缕阳光而已。DDD 是我们共同的语言,写这篇博文的目的就是,希望园友们也可以看到希望,不要再像我一样,迷失自我。

  DDD(领域驱动设计)的过程,从上面一系列的问题更加证明是个迭代过程,一次一次的否决自己,然后再找回自己,反反复复,复复反反,才能成就真正的自己。可能几天或者几周后,看现在的这篇博文就像一坨屎一样,但是没关系,因为我又离真相更进了一步。

  我喜欢这个挑战,我也会坚持的完成它,谁叫我热爱它呢,just so so。

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

  贴一下,当时寻找的领域模型设计资料,仅作参考:

拨开迷雾,找回自我:DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计?

时间: 2024-10-15 13:30:07

拨开迷雾,找回自我:DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计?的相关文章

基于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领域驱动设计的理解

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

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领域驱动设计实践篇之如何提取模型

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

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

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