DDD 领域驱动设计-看我如何应对业务需求变化,领域模型调整?

时间:2021-02-08 08:07:36

写在前面

上一篇: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 了,感兴趣的话,可以参考下。

写在最后

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

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