写在前面

上一篇:DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

“愚蠢的应对”,这个标题是我后来补充上的,博文中除了描述需求变化、愚蠢应对和一些思考,确实没有实质性的应对,文不对题,实在惭愧。

这次应对,我们从领域模型开始。

领域模型思考

业务需求变化,关于领域模型的调整,上一篇我只给出了一些思考,但这段内容,我觉得是那篇博文最重要的地方,不知道你仔细看了没,我一直在强调“回复的概念”,以及之前领域模型没有“回复”所造成的一些问题,在上一个版本的领域模型中,对消息的操作,除去本身状态的改变,就只有发送消息操作,也就是 ISendMessageService 领域服务接口,在短消息应用场景中,是有回复和转发操作的,但之前的设计把回复和转发的消息也看作是“消息”,一个完整的“消息”,它和第一次发送的消息是一样的,说是完整,其实也是独立的,我们可以对它进行独立操作,但这也就造成了回复概念的模糊,比如应用层中的 SendMessage/ReplyMessage/ForwardMessage 操作,大部分都是重复代码,最后调用同一个 ISendMessageService 领域服务实现,从应用层的这部分代码,就可以看出,现在领域模型所暴露出来的一些问题。当然不止这些,后来在“愚蠢的应对”中,最后所出现的性能问题,追根究底也是这个原因,因为现在消息的“独立性”,致使消息之间没有任何关联,所以我们在消息仓储进行取出消息对象操作,这个就像是进行所有消息对象的过滤,我所规定的过滤条件是标题和收发件人,然后再进行发送时间降序排序,取出最新发送的那一条消息,当然条件和转换还有很多,最后使用 ORM 生成了一大串的 SQL 代码,然后就优化,再优化,最后就陷入泥潭了。。。

所有的一些问题出现,追根究底都是领域模型设计不合理所导致的,从这一方面就可以体现出领域模型的重要性

回复是现在领域模型调整的核心,在上一篇博文评论中,针对现有领域模型的调整,我和 ntefocus 探讨了两种方式,还有后来徐少侠、翱翔提出的第三种方式,这边我再大致总结一下,详细内容可以看上一篇的评论。

领域模型调整的三种方式

  1. 消息和回复设计成两个实体(消息为聚合根),发送消息就是针对第一个实体而言,之后回复就是针对第二个实体,可以很好进行区分,在仓储获取的时候也更方便。以前,每个消息都是独立的消息,回复也是一个消息,也就是说,回复也有对应的收件人;现在不同了,只有第一个才是消息,后面的都是该消息的回复,回复不需要指定收件人,只需要指定被回复的消 ID即可;所以,如果是要这样的需求,那这个消息系统的领域模型和论坛就很像了,但不完全是一个论坛系统,还是有一定的差别,比如论坛里的帖子是没有收件人的概念的,而这里的消息有收件人的概念。

    然后,针对类似论坛的领域模型,消息和回复是两个实体了;

    消息:id, subject, body, senderId, receiverId

    回复:id, messageId, body, replierId
  2. 基本上不动现在的消息领域模型,发送和回复在实体中用标识进行区分,那发送消息和回复消息进行关联呢?可以加一个自关联消息实体对象,表示它回复的是哪条消息,这种方式虽然很。。。但是以后的扩展会比较好应对,比如后面有转发功能,就是消息增加一种转发标识就行了,而且这种不会感觉到很怪,回复和发送都是一种消息,同属于消息实体。

    只要扩从一下消息实体,增加一个parentId,表示当前消息的父消息是什么,这个parentId可以为空;顶层消息的parentId为空,回复消息的parentId是其被回复的消息的id。然后replierId就填到senderId里,收件人ID你在回复的时候肯定也能得到。然后对于回复,就不要填写subject了;

    通过这样的实体变动,那消息实体就变为:id, subject, body, parentId, senderId, receiverId
  3. session模式(session看作是一个概念,描述可能不是很准确),发送消息时,会有一个消息,同时生成一个会话,该消息自动关联到该会话,也就是消息上会有一个sessionId;然后消息的标题不属于消息本身,而是属于会话;当回复时,你也把回复理解为一个消息,然后这个消息的sessionId也是当前的session的id;这种方式和第一种方式的最大差别,就是是否把消息标题独立到一个独立的会话对象中,大致实体:

    Message: ID, Title, Body, Attach, Creator, DateTime...

    Session: ID, Owner, Title, State...

针对上面每一种所出现的细节问题,我们探讨了很久,可能我描述的不是很准确,这边只需要明白每一种所表达的意思,针对其实现,没谁对谁错,只有适不适合,每一种改变,都会对应一个新的领域模型产生。

最后,我所采用的是第二种方式,原因在评论中也有详细的说明,这边我再简单叙述下:

  1. 坚守回复也是消息,没有偏离最初的设计。
  2. 可以和现有领域模型很好兼容。
  3. 可以很好应对以后消息模型的调整。
  4. 相对而言改动较小。
  5. ...

当然这种方式最大的缺点就是回复属性的冗余,有利有弊,没有什么完美的,我们来看一下领域模型的具体调整。

领域模型调整

我直接贴一下领域模型的调整代码,首先是 Message 实体中,增加 ParentMessage 属性,用来表示回复的概念:

public Message ParentMessage { get; set; }

这边需要注意的是,ParentMessage 属性类型为 Message,而不是之前描述的 parentId,Id 标识的概念具体会体现在 ORM 映射中,领域模型中应该是 Message 对象。

回复操作,我设计为 ReplySiteMessageService 领域服务,示例代码为:

using CNBlogs.Msg.Domain.Entity;
using CNBlogs.Msg.Domain.ValueObject;
using CNBlogs.Msg.Infrastructure; namespace CNBlogs.Msg.Domain.DomainService
{
/// <summary>
/// ReplySiteMessageService 领域服务-回复消息
/// </summary>
public class ReplySiteMessageService
{
public bool ReplySiteMessage(Message parentMessage, Message replyMessage)
{
if (parentMessage.Sender == replyMessage.Sender && parentMessage.Recipient == replyMessage.Recipient)
{
if (parentMessage.DisplayType == MessageDisplayType.Outbox)
{
parentMessage.DisplayType = MessageDisplayType.OutboxAndInbox;
}
else
{
throw new CustomMessageException("消息已被您删除,无法回复");
}
}
else if (parentMessage.Sender == replyMessage.Recipient && parentMessage.Recipient == replyMessage.Sender)
{
if (parentMessage.DisplayType == MessageDisplayType.Inbox)
{
parentMessage.DisplayType = MessageDisplayType.OutboxAndInbox;
}
else
{
throw new CustomMessageException("消息已被您删除,无法回复");
}
}
else
{
throw new CustomMessageException("您不是收发件人,没有权限回复");
}
replyMessage.ParentMessage = parentMessage;
return true;
}
}
}

上面 ReplySiteMessageService 领域服务中,我只写了一个权限判断的代码,可能以后会有所拓展,如果你熟悉之前 SendSiteMessageService 领域服务的代码,就会发现它们是有所不同的,这也就是发送和回复要进行隔离开,但它们本质都是消息,也就是同一个实体概念。

为了方便大家查看短消息领域模型的代码,我把它上传到 GitHub 了,感兴趣的话,可以参考下。

写在最后

领域模型是领域驱动设计的核心!

在领域驱动设计的过程中,上面那句话常常挂在嘴边,但当实际操作的时候,却往往会把它忽略,这次领域模型调整的代码虽然很少,但是却思考了很久,核心内容确定下来,后面的一些操作才能够围绕它展开。

DDD 领域驱动设计-看我如何应对业务需求变化,领域模型调整?的更多相关文章

  1. DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

    写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...

  2. DDD 领域驱动设计-看我如何应对业务需求变化?

    tks: http://www.cnblogs.com/xishuai/p/3972802.html

  3. DDD领域驱动设计

    DDD领域驱动设计实践篇之如何提取模型 需求说明: 省级用户可以登记国家指标 省级用户和市级用户可以登记指标分解 登记国家指标时,需要录入以下数据:指标批次.文号.面积,这里省略其他数据,下同 登记指 ...

  4. DDD领域驱动设计落地实践(十分钟看完,半小时落地)

    一.引子 不知今年吹了什么风,忽然DDD领域驱动设计进入大家视野.该思想源于2003年 Eric Evans编写的"Domain-Driven Design领域驱动设计"简称DDD ...

  5. DDD 领域驱动设计-在动手之前,先把你的脑袋清理干净

    惨不忍睹的翻译 英文原文:http://www.codeproject.com/Articles/339725/Domain-Driven-Design-Clear-Your-Concepts-Bef ...

  6. C#进阶系列——DDD领域驱动设计初探(一):聚合

    前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的 ...

  7. C#进阶系列——DDD领域驱动设计初探(六):领域服务

    前言:之前一直在搭建项目架构的代码,有点偏离我们的主题(DDD)了,这篇我们继续来聊聊DDD里面另一个比较重要的知识点:领域服务.关于领域服务的使用,书中也介绍得比较晦涩,在此就根据博主自己的理解谈谈 ...

  8. DDD领域驱动设计初探

    DDD领域驱动设计初探1 前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下D ...

  9. [转] DDD领域驱动设计框架分享

    从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven desi ...

随机推荐

  1. Master-Slave通用基础框架

    一.设计目的 设计出一个通用的Master-Slave基础框架,然后可以基于这个框架来实现特定的业务需求,比如实现多节点并行计算.分布式处理等. 二.设计理念 基于经典的命令模式,Master和Sla ...

  2. 从问题看本质:socket到底是什么?

    一.问题的引入——socket的引入是为了解决不同计算机间进程间通信的问题 1.socket与进程的关系 1).socket与进程间的关系:socket   用来让一个进程和其他的进程互通信息(IPC ...

  3. (转)对博士学位说永别 by 王珢

    对博士学位说永别 by 王垠 经过深思熟虑之后,我决定再次“抛弃”我的博士学位.这是我第三次决定离开博士学位,也应该是最后一次了.这应该不是什么惊人的消息,因为我虽然读博士10年了,可是我的目标从来就 ...

  4. iOS8中定位服务的变化(CLLocationManager协议方法不响应,无法回掉GPS方法,不出现获取权限提示)

    最近在写一个LBS的项目的时候,因为考虑到适配iOS8,就将项目迁移到Xcode6.0.1上,出现了不能正常获取定位服务权限的问题. self.manger = [[CLLocationManager ...

  5. Mariadb数据库设置及操作 一主多从 备份还原(实测笔记)

    环境: 系统硬件:vmware vsphere (CPU:2*4核,内存2G,双网卡) 系统版本:CentOS-7-x86_64-Minimal-1611.iso 数据库版本信息 : 10.1.20- ...

  6. Hive_配置远程Metastore

    注 : 待测试 一.准备两三台linux机器,最好是hadoop集群环境 机器A:10.0.0.2 机器B:10.0.0.3 机器C:10.0.0.4 二.个机器安装信息 机器A安装mysql(用于存 ...

  7. C代码实现非循环单链表

    C代码实现非循环单链表, 直接上代码. # include <stdio.h> # include <stdlib.h> # include <malloc.h> ...

  8. 关于DOM的一些笔记(二)

    1.选择符API (1).querySelector()方法 querySelector()方法接受一个CSS选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null. 通过Docu ...

  9. Java虚拟机

    虚拟机每次方法的调用和返回都伴随着栈帧的入栈和出栈,而每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用(表明该栈帧执行的是哪个方法),持有这个引用是为了支持方法调用中的动态连接.这些符号引用中 ...

  10. C# WinForm 中英文实现, 国际化实现的简单方法

    来源:http://www.jb51.net/article/45675.htm,今天看到了借鉴过了,保存一下,下次开发直接用嘻嘻 软件行业发展到今天,国际化问题一直都占据非常重要的位置,而且应该越来 ...