领域驱动设计的核心-DomainModel(领域模型),这个大家都懂,不过上一次关于领域模型的设计分享还是两个月前,中间还有一些东西是没有的,比如存储上的纠缠等等,说这些东西不重要,其实挺重要的,因为是一个完整的应用必须要考虑的东西(Demo除外),但相对于领域模型,在领域驱动设计中是最重要的。我在这篇博文中分享的思路是:一个特定的业务场景,一个真实项目的业务需求发生变化,应用领域驱动设计,我该如何应对???注意:上面我用的是问号,所以一定有一些“坑”,大家在阅读过程中要“小心”。具体业务场景具体业务场景?没错,就是我们熟悉的博客园网站上的短信。详见:【网站公告】8月17日(周日下午)14:00-15:00将发布新版站内短信。距离上个版本发布已经一个多月了。说是“新版”,其实就是重写了之前短信的代码,然后用领域驱动设计的思想去实现。界面换了个“位置”,功能和原来没太大变化。发布后,出现了很多问题,比如前端界面、数据库优化、代码不规范等等。有些技术问题可以很快解决,比如数据库索引优化等。但是有些问题,比如SELECTFileName,因为程序代码是基于领域驱动设计思想实现的,那么不能直接写selectfilename1,filename2,filename2...fromtablenameSQL代码,所以实现起来要花很多心思,这个很头疼。我为什么要问这些问题?由于这些问题,它们只出现在实际应用项目中。如果做一个领域驱动设计的简单demo,会不会出现数据库性能问题?肯定不会,那你就不会去想一些存储的问题,更不会去想一些变化,所以领域驱动设计的进步,只有你自己去实际使用,而不是仅仅做一些演示,在实际应用的过程中,去发现问题和解决问题,我觉得这样会更有价值。关于短信的业务场景,其实我之前写的一些领域驱动设计的博文都是围绕它展开的。很多园友认为这个业务场景是我杜撰的,就像之前网焦哥问我:“你说什么?”这条短信和博客园里的短信是不是很像?”,我回答:“是!”,呵呵。后来发现,虚构的业务场景有时候很难说明问题。比如JesseLiu之前在一篇博文中提到了一个用户注册的问题,这个问题已经讨论了很久了,但是最后的结果呢?我觉得没有结果,因为业务场景是虚构的,所以会造成“公众是对的,女人是对的”,以至于大家很难达成一些共识点。博客园短信的业务场景再真实不过了,毕竟大家都在实际使用,所以不多说了,就是一个用户给另一个用户发消息,然后回复什么的,在之前的一些博文中也有说明,大家可以参考下:我的“第一次”,没了:DDD(领域驱动设计)理论结合实践。业务需求变化目前的博客园短信,为了方便用户看到之前回复的一些内容,我们在底部添加了“===以下为回复信息===”,示意图:该方法其实就是把之前的回复内容替换成新消息的内容,然后作为新消息发送。除了消息内容“冗余”之外,还有一个缺点就是如果两个人回复很多,你会发现消息内容会变成“一坨XX”,不忍直视。后来看不下去了,决定做点改变,模仿iMessage或者QQ的消息方式,示意图:这种方式和上面的“一堆XX”形成了鲜明的对比。对话方式的消息展示让用户的体验更好,就像两个人面对面交谈一样,非常轻松简洁。我想我们这种方式的改变会让你“爱上”我们的短信。对,没错,这就是业务需求的变化。在应用开发的过程中,需求是不断变化的。我们要做的就是不断改进和适应这种需求的变化。当然,每个人的反应都不一样。下面看看我的“笨”应对方式。“愚蠢”的回应我个人认为这个节点的内容非常重要。在领域驱动设计的过程中,也是很多人经常掉进去的一个“坑”,因为我们长期受“脚本模式”的影响,应用需要调整,但你会不自觉地“偏离”,这就偏离了领域驱动设计的思想,最终让你的应用“不伦不类”。当时为了在应用程序中快速实现这个功能,都是在讲技术实现,完全没有用领域驱动的思想去考虑。我是怎么想的?先考虑UI,主要是两个界面:消息列表:收件箱、发件箱和未读消息列表。消息详情:消息详情页面。在实现消息列表之前,无论是发送、回复还是转发,该短信都作为一条新短信发送。“消息上下文”作为消息的附件放置在新短消息的内容中。删除之前发送的消息。在新回复的短信内容中,仍然可以看到之前发送的内容。列表中的显示是单独显示的,但是新的需求变更不能这样操作。这有点像两个人在聊一个话题,里面包含了我们讨论的关于这个话题的所有内容。显示列表时,首先,标题显示的是本主题的标题。就像邮件回复一样,我们可以加上“邮件标题(3)”,这个“3”代表两个人都回复了3次。其实,实践题的逻辑有些不准确。毕竟我们是一个短信项目。我们可以这样想。我给netfocus发了一条消息,标题:“Sayhello”,内容:“hellonetfocus”,然后他回复我:“helloxishuai”,后面可能会有一些消息回复,但都是回复到我发的第一条消息,也就是说后面都是回复,那么这会在消息列表中显示,此时标题显示为“Sayhello(3)”,后面的时间就是***的回复时间,示意图:以上是netfocus的收件箱示意图,收件箱列表显示的逻辑是以发件人和title为身份的,比如JesseLiu,也发了一个“你好”消息到netfocus。虽然标题相同,但发件人不同,因此列表显示两条消息。代码是如何实现这个功能的?贴出代码看看:publicasyncTask>GetInbox(Contactreader,PageQuerypageQuery){varquery=efContext.Context.Set().Where(newInboxSpecification(reader).GetExpression()).GroupBy(m=>new{m.Sender.ID,m.Title}).Select(m=>m.OrderByDescending(order=>order.ID).FirstOrDefault());intskip=(pageQuery.PageIndex-1)*pageQuery.PageSize;inttake=pageQuery.PageSize;returnawaitquery.SortByDescending(sp=>sp.ID).Skip(skip).Take(take).Project().To().ToListAsync();//MessageListDTO是以前的版本剩下的问题(SelectFileName),暂时没动。}GetInbox是MessageRepository中的一个操作。事实上,原始收件箱的代码并不是这样处理的。你会看到现在的代码其实就是Linq的代码拼接。我这样做是为了方便查询。现在看起来像“一堆XX”,代码我就不多说了,上面的列表显示功能是可以实现的,除了回复数的显示,其实你会看到这是为了过滤发件人和标题,然后选择发送时间***该消息。虽然这段Linq代码看起来“简单”,但如果跟踪生成的SQL代码,你会发现它非常臃肿。没有办法,为了实现了功能,那么就得对数据库进行优化,主要是索引优化,这个当时优化了很久,也没有找到合适的优化方案,所以我不得不重新思考这样做是否不合理?这完全是技术驱动,后来发现自己在领域驱动设计的道路上完全“偏离”了,消息详情页业务需求的变化,主要是消息详情页的变化。从上面消息详情页的示意图可以看出,作为menti上面的收件箱列表显示是标题和发件人的筛选,其实详情页是按标题和发件人找出回复邮件,然后按发送时间降序排列。具体操作是在收件箱中点击一条消息,然后通过消息和发件人在仓库中找到该消息的回复消息。示例代码:publicasyncTask>GetMessages(Messagemessage,Contactreader){if(message.Recipient.ID==reader.ID){returnawaitGetAll(Specification.Eval(m=>m.Title==message.Title&&((m.Sender.ID==message.Sender.ID&&m.Recipient.ID==message.Recipient.ID&&(m.DisplayType==MessageDisplayType.OutboxAndInbox||m.DisplayType==MessageDisplayType.Inbox))||(m.Recipient.ID==message.Sender.ID&&m.Sender.ID==message.Recipient.ID&&(m.DisplayType==MessageDisplayType.OutboxAndInbox||m.DisplayType==MessageDisplayType.Outbox)))),sp=>sp.ID,SortOrder.Ascending).ToListAsync();}else{returnawaitGetAll(Specification.Eval(m=>m.Title==message.Title&&((m.Sender.ID==message.Sender.ID&&m.Recipient.ID==message.Recipient.ID&&(m.DisplayType==MessageDisplayType.OutboxAndInbox||m.DisplayType==MessageDisplayType。发件箱))||(m.Recipient.ID==message.Sender.ID&&m.Sender.ID==message.Recipient.ID&&(m.DisplayType==MessageDisplayType.OutboxAndInbox||m.DisplayType==MessageDisplayType.Inbox)))),sp=>sp.ID,SortOrder.Ascending).ToListAsync();不知道你能不能看懂,反正我现在需要好好想想这段代码,呵呵,消息详情页基本就是这样实现的,在应用里面有一些获取“点击消息”等操作层,UI中的消息显示判断等。除了上述列表和详情页面的变化外,消息发送、回复、销毁的实现也需要调整。因为消息域模型没有变化,发送消息还是按照之前的发送逻辑,所以发送消息没有变化,回复消息也没有太大变化,但是回复的时候需要获取消息标题,因为除了第一次发送的消息,需要填写标题,后续回复的消息不需要填写标题,需要添加的无非就是消息的内容。消息销毁的变化比较大,因为在消息独立发送之前,所以每条独立的消息都可以销毁,但是从上面消息详情页的示意图可以看出,独立的消息是不能销毁的,只能销毁完整消息,即详情页底部的删除按钮,示例代码:publicasyncTaskDeleteMessage(intmessageId,stringreaderLoginName)IMessageRepositorymessageRepository=newMessageRepository();(消息编号);if(message==null){returnOperationResponse.Error("抱歉!获取失败!错误:消息不存在");}Contactreader=awaitcontactRepository.GetContactByLoginName(readerLoginName);如果(读者==null){returnOperationResponse。Error("抱歉!删除失败!错误:该运营商不存在");}if(!message.CanRead(reader)){thrownewException("抱歉!获取失败!错误:无权限删除");}message.DisposeMessage(阅读器);varmessages=awaitmessageRepository.GetMessages(消息,阅读器);foreach(Messageiteminmessages){item.DisposeMessage(阅读器);messageRepository.Update(项目);}awaitmessageRepository.Context.Commit();返回操作响应onse.Success("删除成功");}这是应用层的消息销毁操作。可以看到应用层的操作代码很乱。这是实施的代价。除了消息销毁,还有一个操作:消息状态设置,即消息“未读”和“已读”设置,这段代码是在应用层ReadMessage操作中实现的,代码比较乱,我就不贴了,类似于消息销毁操作,只是消息状态设置但是只是设置了一些状态#p#回到一些思考的原点为什么要详细描述我当时实现的想法?其实我想和你我产生共鸣。上面的一些实现操作完全是为了实现而实现的。不同的应用场景下业务需求的变化是不一样的,但是思路大体是想通的,就是说,如果你能正确的处理这个业务需求的变化,那么在另一个应用中还是可以处理的设想。如果不能正确处理,领域驱动设计就是“空话”。为什么?因为领域驱动设计是对业务需求变化的更好响应。其实我们已经实现了上面的需求变化,只是没有发布,就像一个多月前发布公告里说的“你的代码是这样的吗?”如果这样实现,之后的短信代码就是那一团面条,惨不忍睹。一些回溯本源的思考其实就是回到领域模型去看待业务需求的这种变化。关于这部分内容,我还没有做出准确的处理。这里我就说一下我的理解:业务需求的变化,领域模型的变化了吗?首先,在之前的实现中,消息列表显示的内容应该是在应用层体现出来的,所以在领域模型中可以暂时忽略,而在仓库中要考虑到这一点。领域模型怎么了?先说发消息,这个改了吗?我不这么认为。它仍在点对点发送消息。这是以前用SendSiteMessageService域服务实现的,逻辑没有太大变化。回复消息呢?其实我觉得这是对***的改观,如果你看之前的回复代码,我并没有在领域模型中实现回复消息操作,为什么呢?因为我当时认为回复消息其实就是发送消息,所以在应用层回复消息的操作其实就是调用SendSiteMessageService域服务。现在看来,不应该这样实现。我们先梳理一下回复消息操作的处理流程。其实这个上面已经分析过了。除了发送第一条消息外,后面的消息都是回复操作。必须有一个标识符来解释这个消息。回复的是发送的消息,那么这个怎么设计呢?回复消息是否应该设计为实体?还是值对象?我个人认为应该设计成一个实体。想一想就明白其中的道理了。尽管它存在于发送的消息中,但它也是唯一的。比如一个人回复另外两个人同一条消息,那么这两个回复消息应该都是独立存在的,那么如何处理这种依赖关系呢?我们可以在消息实体中添加一个标识符,以指示它正在回复哪条消息。以上确定之后,我们如何实现回复消息的操作呢?我们可以使用一个域服务,比如ReplySiteMessageService,来处理一些回复消息的操作。这可能与SendSiteMessageService域服务有些不同。比如一个人一天只能发200条消息,但是我们不能把这个逻辑放在回复消息字段服务中,回复只给一个人,所以这个没有限制,发给任何人。为了避免广告宣传,我们必须设置一个发送限制。当然,具体实现还是要看需求。要求。除了回复消息的变化之外,再说说消息的状态(未读和已读)和消息销毁。回复内容设置为已读。我们之前的设计是针对一个独立的消息状态,也就是说每条消息都有一个消息状态。这样,我们其实可以把这个状态放在发送消息实体中,如果有人回复,消息状态设置为未读,回复消息没有状态。如果这样设计的话,感觉有点像一个值对象,可以把一个回复消息值对象从消息实体中分离出来。当然,这只是我的一个想法。消息销毁类似于这个消息状态,这里就不多说了。除了这两个变化之外,还有一些细节需要考虑,只能在实现中暴露出来。关于对象读取的额外思考这其实是我读到的关于仓库的可怕实现代码的一些思考。您可以阅读这篇博文:您正在以错误的方式使用ORM。仓储在领域驱动设计中的作用可以看作是实体的存储仓库。我们需要通过入库来获取实体对象。入库的实现可以用任何方式实现,但是传输对象必须是聚合根对象,理论上是没有问题的,但是在实际项目中,我们从仓库中获取对象,一般有两个目的:用于领域模型中的一些验证操作。用于应用层的DTO对象转换。第一个没有问题,但是第二个必然会带来性能问题,也就是上面Jimmy(AutoMapper的作者)提到的SelectN问题。这个我以前也遇到过,***作为解决方案,我按照他在AutoMapper映射中的扩展,也就是上面代码中的Project().To(),但这不免违背了领域驱动设计中存储的一些思想。这个内容我不想多说。重点就是上面的领域模型思想和存储问题。我必须做一些改变,因为它现在的实现让我有强迫症的感觉很不爽,不管是CQRS,ES,还是六边形架构,毕竟还是先尝试实现吧。有问题并不可怕,可怕的是不知道如何改正。写在***领域驱动设计的路上,有很多意想不到的情况发生在你身上。一不留神,就会偏离大方向。不幸的是,我没有针对这个业务需求变化做一些改变。具体实现,但是我觉得很重要的是要意识到这个问题。这篇博文分享,希望能引起大家的共鸣。本文来自:http://www.cnblogs.com/xishuai/p/3972802.html