DDD 回归具体的业务场景,Domain Model 再再重新设计
DDD 回归具体的业务场景,Domain Model 再再重新设计
首先,把最真挚的情感送与梅西,加油!
写在前面
阅读目录:
上一篇《设计窘境:来自 Repository 的一丝线索,Domain Model 再重新设计》。
讲本篇内容之前,先回顾上一篇所讨论的内容,主要是 Repository(仓储)的职责问题,属于领域?还是应用层?其实到头来也没有准确的结论,但是最终比较偏向于仓储定义在领域,实现在基础层,调用在应用层。你可能有些疑问,为什么要讨论仓储的职责问题?看过上一篇的内容你可能会有些答案,这也就是上一篇博文标题,为什么是“设计窘境”的原因。
本篇博文标题定义为:”拨乱反正“,就像战乱纷争的年代,清剿叛军,回归大统,所表达的意思就是,排除一切干扰因素,回归正确的业务场景,然后进行干净的领域模型设计。领域驱动设计这个实践系列已经写了大概六七篇博文了,从领域模型到底如何设计?到领域模型重新设计,到领域模型再重新设计,到现在的领域模型再再重新设计。。。对,被你看出来了,领域驱动设计中最重要的就是领域模型的设计,但是到现在为止,领域模型的设计一直没有完成,而是一次一次的被推翻重建,道路是曲折的,前途是光明的,但是这个过程真是太痛苦了,回顾现有的这个过程,就会发现,为什么领域模型设计这么难?原因就是领域模型中的职责分配问题,谁存在?谁不存在?谁属于谁?谁不属于谁?谁是谁的?谁是谁的谁的谁(好像是一段歌词,不好意思哈,打顺手了)?但这一切的前提都是建立在正确的业务场景之上,那我们这个业务场景究竟是什么?希望你能从本篇博文中找到些许答案。
如果想了解前面大大小小的“坑”,请访问《[17]小菜学习编程-DDD》,如果不想了解(推荐),那就请从本篇博文开始了解吧。
重申业务场景
其实这个项目(MessageManager)一开始设计的时候,定义为短消息系统,也就是类似博客园短消息系统,发送人给接收人发送一个短消息,接收人接收到这个短消息进行查看,发送人和接收人可以查看各自的收发件箱,大概也就是这样的一个业务场景。但是后来才发现真正的业务场景是,短消息系统中的“短”字应该去掉,也就变成了消息系统,不一定只适用于短消息发送,还可能发送邮件、发送短信等,但是这种发送不会像短消息那种需要进行持久化,他们只是一个发送的动作。
有朋友可能看到这可能会有些疑问,发送邮件、发送短信这种信息发送,不应该属于应用层所干的事吗?其实这很容易造成误解,比如一个在线商城应用程序中,业务需求要求每提交一笔订单发送一封邮件给客户,我们一般会在应用层接收来自领域处理完订单的请求后,调用基础层提供的服务完成邮件发送,这种设计是没有什么问题的。需要明确的是现在的业务场景是消息系统,而不是在线商城,聚焦的是消息业务,那所有具体的消息都是业务,也就包含邮件发送和短信发送,因为他们是属于消息的一种。
毫无疑问,我们现在的消息系统就必须要抽离出,所有消息的抽象业务逻辑,以适用于所有消息业务场景的具体应用,我觉得这才是《道德经》中“有之以为利,无之以为用”的真谛所在(以前关于这个观点的解读,现在感觉都是在瞎扯),消息领域模型所展现的就是“无”,体现出来的结果就是“有”。说具体点就是,所有消息应用包含什么东西,我觉得就是三个对象:发送人对象、接收人对象和消息对象,这三个对象组成一个完整的消息系统,不管短消息、邮件和短信都是如此,三者缺一不可,缺少任何一种就不是一个完整的消息系统,可能在不同的消息场景中会有些变化,比如短消息系统中,发送人是一个用户对象,但是在邮件和短信系统中,发送人只是一个邮箱和手机号标识,但是它也代表着发送人所表达的意义。
除了抽离出消息系统所存在的对象,还要去了解整个消息业务场景中所表现的过程,在消息系统中最重要的一个用例就是消息发送,因为查看消息或者查看收发件箱只在短消息业务场景下,邮件和短信的查看可以通过电子邮箱和手机查看,在消息系统中只存在发消息这个业务,那发消息的流程是什么?首先必须有发件人(标识),填写一个消息,贴上收件人(标识),然后送给邮递员进行邮递,短消息、邮件和短信发送都是这个过程,那我们再来看看下之前用户实体的设计:
1 /**
2 * author:xishuai
3 * address:https://www.github.com/yuezhongxin/MessageManager
4 **/
5
6 using System;
7 using System.Collections.Generic;
8
9 namespace MessageManager.Domain.Entity
10 {
11 public class User : IAggregateRoot
12 {
13 public User(string loginName, string displayName)
14 {
15 if (string.IsNullOrEmpty(loginName))
16 {
17 throw new ArgumentException("loginName can't be null");
18 }
19 if (string.IsNullOrEmpty(displayName))
20 {
21 throw new ArgumentException("displayName can't be null");
22 }
23 this.ID = Guid.NewGuid().ToString();
24 this.LoginName = loginName;
25 this.DisplayName = displayName;
26 this.SendMessages = new List<Message>();
27 this.ReceiveMessages = new List<Message>();
28 }
29
30 public string ID { get; set; }
31 public string LoginName { get; private set; }
32 public string DisplayName { get; private set; }
33 public virtual ICollection<Message> SendMessages { get; set; }
34 public virtual ICollection<Message> ReceiveMessages { get; set; }
35
36 public void SendMessage(User receiveUser, Message message)
37 {
38 this.SendMessages.Add(message);
39 receiveUser.ReceiveMessage(this, message);
40 }
41 private void ReceiveMessage(User sendUser, Message message)
42 {
43 this.ReceiveMessages.Add(message);
44 }
45 }
46 }
在之前的设计中,我们把用户设计成一个实体,而且是一个独立于消息的聚合根,因为需要通过其他属性获取到用户,但是在现在的消息业务场景中,用户是不需要存储的,也就是说用户的获取或验证都是通过外部实现的,很显然现在的这种设计就有点不合理了。还有就是用户实体下的 SendMessages 和 ReceiveMessages 属性,用来表示此用户下的发件箱和收件箱,如果存在用户实体,这种设计也是有待商榷的,更何况用户实体并不存在。还有后面加的 SendMessage 和 ReceiveMessage 方法,表示用户发送消息和接收消息的一种行为,为什么要这样设计?主要原因还是来自上一篇的讨论:
《领域驱动设计》账户转账示例流程分享(来自 hailants):
用户发起业务,界面层调用应用层的转账操作
应用层调用 a123 的转账操作,传入对方账户和转账金额
账户 a123 调用 a234 加钱操作
a234 将操作添入事务单元,向账户 a123 返回确认
账户 a123 调用自身减钱操作,添入事务单元,向应用层返回确认
应用层提交事务,向界面层返回确认
相应推理出发送消息业务流程:
用户发起发消息业务请求,界面调用应用层转账操作(传入参数为:标题,内容,发件人名称,收件人名称)
应用层首先创建一个发件人对象(通过仓储获得),然后再创建收件人对象和消息对象
发件人对象调用用户实体中的 SendMessage 操作(参数为收件人对象和消息对象)
在发件人对象中的 SendMessage 方法中,收件人对象调用用户实体中的 ReceiveMessage 操作(参数为发件人和消息对象)
在以上操作的完成后添加到事务操作(具体就是往消息仓储中添加消息领域对象)
应用层提交事务,向界面返回确认。
其实这种设计某种意义上也没有什么问题,至少在短消息系统中,因为发消息本身就是用户的一种行为,但是如果仔细一想就会觉得有些别扭,首先账户转账业务流程和发消息业务流程虽然表面上相似,但是其聚合的对象并不相同,比如账户转账示例中,聚合的是账户,发消息业务场景中如果这样分析应该聚合的是用户,但是很显然并不是,聚合的是消息对象,如果聚合用户就会变成用户消息系统了,这就偏离了大方向,并不是我们所想看到的。还有就是现在的这种设计只适用于短消息业务场景,在邮件和短信业务场景中并不适用,为什么?因为邮件发送和短信发送并不存在用户的概念(这个用户概念并不是现实生活中的人,而是系统中的用户,这个观点很容易造成误解),有的只是一个标识(电子邮件或手机号),用来体现出发件人和收件人的概念,也就是说这种标识并不是一个对象(没有行为的对象),准确的来说应该不是一个实体,那是什么?在领域驱动设计中设计为值对象(为什么要设计成值对象,后面领域模型设计中进行说明),一个没有行为的对象中加入行为操作,本身逻辑就存在问题,所以这种设计是有问题的。
回到发消息这个业务用例上,一个消息对象存在意义的前提是拥有标题、内容、发送人(标识)和接收人(标识),当然还存在一些选填元素,但是主要包含这四个元素,缺少任何一种,就不是一个完整的消息对象,也就是说不能用来发送。发送操作不仅仅是一个对象的行为,而应该是消息领域模型提供的一种服务,也就是领域服务,提供各种消息发送的服务,这一点很容易和基础层的消息发送服务搞混,区分他们只需要记住一点:基础层是技术上的实现,领域是业务上的抽象,因为这个业务场景是消息系统,那发消息就是一种业务用例,而并不是一个技术调用方法。
说了这么多,总结一下所描述的消息业务场景:抽象所有消息业务逻辑(包含短消息、邮件和短信等),应用具体的业务场景(比如短消息)。发消息业务用例:发送人(系统用户)填写消息,包含标题、内容、发送人(标识)、接收人(标识),调用(应用层发送请求)服务(领域服务)发送消息,相当于邮递员投递信件,就是这样的一个过程,至少听起来这么简单,实现起来呢?我觉得那是另一方面的问题了,呵呵。
Domain Model 设计
回顾之前领域模型的设计,你会发现完全是一套一套的,也就是说差别很大,造成这种设计的主要原因是领域模型中的边界和职责问题,这也是领域模型设计中最难的一点,如果边界确定和职责分配和上一版本有细微的差别,那设计出来的领域模型会和上一版本完全不一样,就比如用户的边界确定(是实体?还是值对象?),还有就是仓储的职责问题(领域还是应用层?),如果不确定这些因素,设计出来的领域模型就不是真正的领域模型。
仓储的职责问题在上一篇中有过讨论,开头也给过总结,这边就不多说了,其实现在在设计领域模型的时候就要排除一切干扰因素,比如我现在在设计的时候就把表现层、应用层、仓储中的项目卸载掉了,这个解决方案中的项目就只剩基础层、领域模型和领域中的单元测试项目,这样在设计领域模型的时候才能保证其“纯净度”。
在上面重申业务场景节点中,把发送人(标识)或接收人(标识)设计成值对象,为什么要这样设计?我们稍后解读,先说一下实体和值对象的区别,这两个对象的概念网上有很多资料进行参考,但最好还是看下《领域驱动设计》这本书的定义,作者关于实体的解读,重点强调了实体的唯一性,也就是说实体必须通过唯一标识进行区分,比如消息系统中的消息实体,虽然我和同一个人发送同样内容的消息,但是这两个消息就不能用同一个对象进行标识,而是两个具有同样消息内容的不同消息实体,换句话说,我们不仅需要知道消息是什么,而且还要知道消息是哪个。关于值对象的解读,作者主要强调:“值对象就是那些在设计中我们只关心它们是什么,而不关心它们谁是谁的对象。”这是和实体的最好区分,就是说对于值对象,我们只要知道他们是什么就行了,而并不需要他们是哪个,就比如消息系统中的消息状态值对象,包含两个内容:未读和已读,相对于消息实体而言,我们只需要知道消息状态是什么,它所表达的内容(我们并不关心,它从哪里来,到哪里去)。还有就是值对象是一般相对于实体而言的,就是说值对象一般附属在实体上,如果独立于实体,他们就不存在任何意义,就像消息状态值对象,它如果独立于消息实体,就没有什么意义了,因为消息状态只有相对于消息对象而言才有存在的意义。
内容有点多,换个行。
那为什么要把发送人(标识)或接收人(标识)设计成值对象?那我们分析一下消息系统中收发件人,首先需要明确一点的就是,我们设计的是消息系统,并非是用户消息系统,也就是说把用户中的行为剔除掉(SendMessage 和 ReceiveMessage),对象除掉行为之后就只有属性了,如果一个实体中只有属性,是不是所必要的呢?对于消息系统而言,用户是不被存储的,也就是说用户只是在消息系统中作为一个标识,所谓标识就是所表现出来的一个值。比如发邮件业务场景中,用户A(123@gmail.com)给用户B(456@gmail.com)发送了一封邮件,对于消息系统,我需要确定用户A和用户B吗?显然不需要,因为我只要知道 123@gmail.com 这个邮箱给 456@gmail.com 这个邮箱发送了一封邮件就行了,至于是哪个用户发的,在消息系统中并不需要考虑。在短消息业务场景中也是类似,因为短消息系统中的用户概念来自于其他系统,那其他系统对于用户而言肯定有一个唯一标识(比如主键值、用户名、显示名等等),对于消息系统而言,我只要知道这个标识就行了,至于这个标识所代表的是哪个用户,并不需要关心。
把发送人(标识)或接收人(标识)设计成值对象,还有一个重要原因是,如果把发送人(标识)或接收人(标识)设计成实体,那他们可以独立于消息实体存在,但是我们所设计的是消息系统,并不是用户消息系统,用户来自于外部,如果在消息系统中单独存在就有点不伦不类了,还有就是如果用户设计成实体,这些用户实体对象是需要存储的,这就违背了我们的业务需求。如果把用户设计成值对象呢?就符合我们现在的消息业务场景了,因为在消息系统中,我们只需要知道用户是什么,而且用户独立于消息,对于消息系统而言将没有任何意义。
概念理清楚,下面就是具体的设计了。
IContact 抽象接口:
1 /**
2 * author:xishuai
3 * address:https://www.github.com/yuezhongxin/MessageManager
4 **/
5
6 namespace MessageManager.Domain.ValueObject
7 {
8 public interface IContact
9 {
10 string Name { get; set; }
11 }
12 }
Sender-发送人:
Recipient-接收人:
因为发送人和接收人都是联系人的一种,只不过所扮演的角色不同,所以我们可以把他们抽象出来,IContact 接口中只有一个 Name 属性,用来表示我们上面所讨论的标识,用 Name 也更符合现实生活中的名称(发送人和接收人)。
消息领域模型中的对象确定后,下面就是发送消息服务了,因为消息业务场景中,不只包含短消息的发送,还有邮箱发送和短信发送等,所以我们需要把消息领域服务抽象出来。
ISendMessageService 发送消息领域服务:
1 /**
2 * author:xishuai
3 * address:https://www.github.com/yuezhongxin/MessageManager
4 **/
5
6 using MessageManager.Domain.Entity;
7 namespace MessageManager.Domain.DomainService
8 {
9 public interface ISendMessageService
10 {
11 bool SendMessage(Message message);
12 }
13 }
SendShortMessageService 短消息领域发送服务实现:
除了短消息发送服务,我们还可以实现邮箱发送服务(业务上的 SendMailMessageService),其内部可以调用基础层的发送邮件服务(技术上的 SendMailService),这两个概念容易搞混,需要区分开。通过这个消息领域服务,我们可以在其他的应用程序中进行调用,使用什么消息发送,只需要在调用的时候注入相应的接口实现即可,为什么这么厉害?因为它是所有消息发送的抽象描述,哈哈。
我们再来看下单元测试代码:
1 /**
2 * author:xishuai
3 * address:https://www.github.com/yuezhongxin/MessageManager
4 **/
5
6 using MessageManager.Domain.DomainService;
7 using MessageManager.Domain.Entity;
8 using MessageManager.Domain.ValueObject;
9 using System;
10 using Xunit;
11
12 namespace MessageManager.Domain.Tests
13 {
14 public class MessageDomainTest
15 {
16 /// <summary>
17 /// 消息发送-短消息
18 /// </summary>
19 [Fact]
20 public void DomainTest_SendShortMessage()
21 {
22 ISendMessageService sendMessageService = new SendShortMessageService();
23 IContact sender = new Sender("sender");
24 IContact recipient = new Recipient("recipient");
25 Message message = new Message("title", "content ", sender, recipient);
26 Assert.True(sendMessageService.SendMessage(message));
27 }
28 }
29 }
从单元测试的代码我们可以很清楚的描述发消息这个业务用例,首先创建一个发送短消息的领域服务对象,然后分别创建发送人和接收人对象,标识分别为:sender 和 recipient,下面创建一个消息对象,然后调用领域服务传入消息对象参数进行发送,完成整个的发消息业务。虽然看起来简单,也很容易造成误解,有人也会怀疑领域模型就是这样?其实就是这样,测试用例代码描述的就是业务用例,它体现出来的就是领域模型,当然,SendMessage 方法中只有一段代码,但是它所表达的就是这个业务场景的具体体现。
有时候领域模型设计不出来,可以先写领域模型测试用例的伪代码,因为领域模型的测试用例反应的就是业务需求,一个完成业务场景的具体过程,这也是一种开发的方式,DDD+TDD=?(某一方面的相加)
后记
有朋友可能会觉得:如此简单的业务场景,使用领域驱动设计开发,会不会太简单了?或者说根本不适合?我只想说:大哥,如果你觉得简单,请收了我,可好?
其实任何存在具体的业务场景,不管简单或不简单,都可以使用领域驱动设计开发,领域驱动设计应对的是复杂度,这个复杂度可以理解为未来的复杂度,如果在前期开发的时候,领域模型设计的好,那么后面改东西就会很方便,当然到现在为止,我还只是道听途说,并没有真正体会它的好处,但是我很期待,我想那种感觉应该会很美妙。
关于本篇博文内容就是这些,不管错与对,欢迎大家讨论,只有这样,大家才可以学到更多,不只是你我哦。
MessageManager 项目开源地址:
- 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/小菜
如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^
DDD 回归具体的业务场景,Domain Model 再再重新设计的更多相关文章
- 拨乱反正:DDD 回归具体的业务场景,Domain Model 再再重新设计
首先,把最真挚的情感送与梅西,加油! 写在前面 阅读目录: 重申业务场景 Domain Model 设计 后记 上一篇<设计窘境:来自 Repository 的一丝线索,Domain Model ...
- 拨开迷雾,找回自我:DDD 应对具体业务场景,Domain Model 到底如何设计?
写在前面 除了博文内容之外,和 netfocus 兄的讨论,也可以让你学到很多(至少我是这样),不要错过哦. 阅读目录: 迷雾森林 找回自我 开源地址 后记 毫无疑问,领域驱动设计的核心是领域模型,领 ...
- 一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?
写在前面 阅读目录: 问题根源是什么? <领域驱动设计-软件核心复杂性应对之道>分层概念 Repository(仓储)职责所在? Domain Model(领域模型)重新设计 Domain ...
- No zuo no die:DDD 应对具体业务场景,Domain Model 重新设计
写在前面 上联:no zuo no die why you try 下联:no try no high give me five 横批: let it go上联:no zuo no die why y ...
- DDD 应对具体业务场景,Domain Model 重新设计
DDD 应对具体业务场景,Domain Model 重新设计 写在前面 上联:no zuo no die why you try 下联:no try no high give me five 横批: ...
- DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计?
DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计? 写在前面 阅读目录: 迷雾森林 找回自我 开源地址 后记 毫无疑问,领域驱动设计的核心是领域模型,领域模型的 ...
- DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?
DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)? 阅读目录: 问题根源是什么? <领域驱动设计-软件核心复杂性应对之道>分层概念 Repositor ...
- DDD 领域驱动设计-如何完善 Domain Model(领域模型)?
上一篇:<DDD 领域驱动设计-如何 DDD?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 阅读目录: ...
- 贫血模型;DTO:数据传输对象(Data Transfer Object);AutoMapper ;Domain Model(领域模型);DDD(领域驱动设计)
====================== 我自己的理解 ========================== 一: DTO 我自己的理解,就是 比如你有一个类,跟数据库的table表结构一模一 ...
随机推荐
- 批处理删除IIS的everyone、users的访问权限
原文 批处理删除IIS的everyone.users的访问权限 以下批处理代码功能,实现的是,删除C盘的everyone.users用户对IIS的权限. 一.删除C盘的everyone的权限 cd/ ...
- <%%>创建内联代码块(表达)
其实<%%>很早之前见过它,将一个小的功能仅.别人不理解.今天偶尔,我们看到它的真面目,今天,给大家分享. 语法 代码块呈现(<%%>)定义了当呈现页时运行的内联代码或内联表达 ...
- 检测ORACLE方法汇总数据块损坏
1:使用初始化参数 使用初始化参数db_block_checksum\db_block_checking能够设置数据库对块的物理一致性和逻辑一致性检查. Db_block_checksum:物理一致性 ...
- shell文字过滤程序(十一):paste命令
[版权声明:转载请保留源:blog.csdn.net/gentleliu.Mail:shallnew at 163 dot com] 由于可以从字面上可以看出.paste指挥和cut相反的命令.cut ...
- linux_vim_快捷键
1.vim ~/.vimrc 进入配置文件 如果不知道vimrc文件在哪,可使用 :scriptnames 来查看 set nu #行号 set tabstop=4 #一个tab为4个空格长度 set ...
- Android Volley彻底解决(三),定制自己Request
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17612763 经过前面两篇文章的学习,我们已经掌握了Volley各种Request ...
- enq: SQ - contention
--每分钟操作 SQL> select sql_id, mi, count(mi) 2 from (select event, sql_id, to_char(sample_time ...
- CSharp设计模式读书笔记(13):代理模式(学习难度:★★★☆☆,使用频率:★★★★☆)
代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问. 模式角色与结构: 示例代码: using System; using System.Collections.Generi ...
- Robot Framework + appium 启动手机浏览器的两个方法(1)
一.Open Browser启动 使用Selenium2Library的Open Browser方法,例子如下: browser=手机浏览器类型,如chrome 二.Open Application启 ...
- Errors occurred during the build. Errors running builder 'JavaScript Validator' on
eclipse又一次编译时候就会报错Errors occurred during the build. Errors running builder 'JavaScript Validator' on ...