DDD的基本概念 — 领域事件与集成事件

我们在进行系统开发的时候,经常会遇到”当发生某事件的时候,执行某个动作”。

比如,在一个问答系统中,当有人回复了提问者的提问的时候,系统就向提问者的邮箱发送通知邮件,如果我们使用事务脚本的方式来实现这个功能,就会用到如下代码所示的伪代码:

C#
void 保存答案(long id,string answer)
{
  保存到数据库(id,answer);
  string email = 获取提问者邮箱(id);
  发送邮件(email,"你的问题被回答了");
}

这样编写的代码有如下几个问题。

问题一:

代码会随着需求的增加而持续膨胀。

比如网站又增加了一个功能”如果用户回复的答案中有疑似违规的内容,则先把答案隐藏,并且通知管理员进行审核”,那么我们就要把”保存答案”方法修改成如下代码所示的形式:

C#
void 保存答案(long id,string answer)
{

   long aId = 保存到数据库(id,answer);
   if(检查是否疑似违规(answer))
   {
      隐藏答案(aId);
      通知管理员审核();
   }
   else
   {
      string email = 获取提问者邮箱(id);
      发送邮件(email,"你的问题被回答了");
   }
   
}

随着系统的升级,这个方法的代码页越来越长、越来越复杂,充斥着大量一层嵌套一层的判断语句。

这样的代码警告几任开发人员的接手,可能没有任何一个开发人员能够完全理解这个方法。

当需要为这个方法增加新的功能的时候,开发人员不敢修改前任的代码,他只能”胆战心惊”地找到一个位置插入自己编写的代码,如果代码恰好能够运行,又没有导致原有功能出现bug,就是一件”天大的喜事”。

这样新版本的代码又成为了继任的开发人员不敢动的”祖传代码”。

总之,这样的事务脚本的可读性和可维护性非常差。

问题二:

代码可扩展性低。

在后续版本中,我们可能要把”发送邮件”改成”发送短信”,那么我们就要把上述代码改成与发送短信相关的代码;

如果后续我们又要把逻辑改成”向普通会员发邮件,向VIP会员发短信”,那么我们就要把上述代码改成由多个判断语句组成的代码块。

面向对象设计中有一个原则是”开闭原则“,即”对扩展开放,对修改关闭”,通俗来讲就是”当需要增加新的功能的时候,我们可以通过增加扩展代码来完成,二不需要修改现有的代码”。

很显然,我们这种事务脚本的写法是很难满足开闭原则的。

问题三:

用户体验很差。

这段代码中除了”保存答案”这个核心的业务逻辑之外,掺杂了”检查是否疑似违规””发送邮件”等业务逻辑,这些业务逻辑的执行一般都比较耗时,会拖慢”保存答案”方法的执行速度,造成每次用户单击【保存答案】按钮的时候都要等待很长时间。

问题四:

容错性差。

“检查是否疑似违规”可能需要调用第三方的鉴黄服务,”发送邮件”需要访问邮件服务,这些都需要访问外部系统,这些外部系统并不总是稳定的。

比如,在发送邮件时,邮件服务器的暂时故障可能会造成用户单击【保存答案】按钮后,系统提示”操作失败”,因此用户体验是极差的。

为了解决这些问题,我们可以在保存答案后,发出一个”答案已保存”的通知事件,内容审核模块和邮件发送模块监听这个事件来分别进行各自的处理。

采用事件机制的伪代码如以下代码所示:

C#
void 保存答案(long id,string answer)
{
   long aId = 保存到数据库(id,answer);
   发布事件("答案已保存",aId,answer);
}

[绑定事件("答案已保存")]
void 审核答案(long aId,string answer)
{

   if(检查是否疑似违规(answer))
   {
       隐藏答案(aId);
       发布事件("内容待审核",aId);
   
   }

}

[绑定事件("答案已保存")]
void 发邮件给提问者(long aId,string answer)
{
    
    long qId = 获取问题Id(aId);
    string email = 获取提问者邮箱(qId);
    发送邮件(email,"你的问题被回答了")

}

   
   

采用这样的事件机制的代码有如下优点:

  • 关注点分离:3个方法各司其职,各种的业务逻辑没有混杂到一起,代码的可读性、可维护性都非常高。
  • 扩展容易:如果我们需要实现”保存答案后,刷新缓存”,只要再增加一个新的方法并且将其绑定到”答案已保存”事件即可,现有的代码不用做任何修改,符合”开闭原则”
  • 用户体验好:我们可以把”审核答案””发邮件给提问者”等这些对事件的处理一部运行,这样这些处理就不会影响用户体验。
  • 容错性更好:如果外部系统调用失败,我们可以进行失败重试或者服务降级等处理。

在我们对用户需求进行分析的时候,如果发现有”如果发生了某事,则执行某个动作”这样的描述的时候,我们都可以把它们通过事件机制来实现。

DDD中的事件分为两种类型:

领域事件(domain events)和集成事件(integration events)。

聚合内一般不需要通过领域事件进行事件传递,领域事件主要用于在同一个微服务内的聚合之间的事件传递,而集成事件用于跨微服务的事件传递。

比如在问答微服务中,当用户保存答案的时候,审核答案的逻辑我们一般通过领域事件来实现。

如果项目中有专门的邮件发送微服务,则当用户保存答案的时候,发送邮件给提问者的操作就要通过集成事件来实现。

领域事件由于是在同一个进程内进行的,我们通过进程内的通信机制就可以完成;

集成事件由于需要跨微服务进行通信,我们就要引入事件总线(eventbus)来实现事件的传递。

我们一般使用消息队列服务器中的”发布/订阅”模式来实现事件总线。

订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
滚动至顶部