DDD(二)聚合、聚合根、领域服务、应用服务、仓储”和“工作单元”、领域事件、集成事件

DDD之聚合、聚合根

聚合(Aggregate)

1、目的:高内聚,低耦合。有关系的实体紧密协作,而关系很弱的实体被隔离。
2、把关系紧密的实体放到一个聚合中,每个聚合中有一个实体作为聚合根(Aggregate Root),所有对于聚合内对象的访问都通过聚合根来进行,外部对象只能持有对聚合根的引用。
3、聚合根不仅仅是实体,还是所在聚合的管理者。

高内聚,低耦合

image-1660405791559

聚合的意义

1、为什么聚合可以实现“高内聚,低耦合”。
2、聚合体现的是现实世界中整体和部分的关系,比如订单与订单明细。整体封装了对部分的操作,部分与整体有相同的生命周期。部分不会单独与外部系统单独交互,与外部系统的交互都由整体来负责。

好处就是聚合内部的实体可以紧密的工作,聚合之间可以低耦合的工作

聚合的划分很难

1、系统中很多实体都存在着不同程度的关系,这些关系到底是设计为聚合之间的关系还是聚合之内的关系是很难的。
2、聚合的判断标准:实体是否是整体和部分的关系,是否存在着相同的生命周期。
3、订单与订单明细?用户与订单?

聚合的划分没有标准答案

1、不同的业务流程也就决定了不同的划分方式。
2、新闻和新闻的评论?

例子:

​ 传统的新闻网站可以把新闻和对应的新闻评论设计成一个聚合,但是现在大多数新闻网站都有热评榜,这就导致新闻评论是可以单独与外部系统交互的,这就可以设计成2个聚合了。所以得根据自己的系统来合理的划分。

聚合的划分的原则

1、尽量把聚合设计的小一点,一个聚合只包含一个聚合根实体和密不可分的实体,实体中只包含最小数量的属性。
2、小聚合有助于进行微服务的拆分。

聚合宁愿设计的小一点也不要设计的太大

DDD之领域服务、应用服务

1、聚合中的实体中没有业务逻辑代码,只有对象的创建、对象的初始化、状态管理等个体相关的代码。
2、对于聚合内的业务逻辑,我们编写领域服务(Domain Service),而对于跨聚合协作以及聚合与外部系统协作的逻辑,我们编写应用服务(Application Service)。
3、应用服务协调多个领域服务、外部系统来完成一个用例。

在DDD中,一个典型的用例的处理流程如下

第一步,准备业务操作所需要的数据。
第二步,执行由一个或者多个领域模型做出的业务操作,这些操作会修改实体的状态,或者生成一些操作结果。
第三步,把对实体的改变或者操作结果应用于外部系统。

用例方便理解的就是如下:
//第一步,准备业务操作所需要的数据。 
MyUser user = new MyUser();
   
//第二步 我这里是新增用户 如果是修改上面应该准备好数据 这里只需要修改就行
//这里做业务处理
 	user.UserName = req.UserName;
     user.PasswordHash = req.Password;
     user.CreationTime = DateTime.UtcNow;
	 ctx.add(user);
//第三步,把对实体的改变或者操作结果应用于外部系统。
	ctx.SaveChanges();
职责的划分

1、领域模型与外部系统不会发生直接交互,即领域服务不会涉及数据库操作。
2、业务逻辑放入领域服务,而与外部系统的交互由应用服务来负责。
3、领域服务不是必须的,在一些简单的业务处理中(比如增删改查)是没有领域知识(也就是业务逻辑)的,这种情况下应用服务可以完成所有操作,不需要引入领域服务。这样可以避免过度设计。

“仓储”(Repository)和“工作单元”(Unit Of Work)

1、仓储负责按照要求从数据库中读取数据以及把领域服务修改的数据保存回数据库。
2、聚合内的数据操作是关系非常紧密的,我们要保证事务的强一致性,而聚合间的协作是关系不紧密的,因此我们只要保证事务的最终一致性即可。
3、聚合内的若干相关联的操作组成一个“工作单元”,这些工作单元要么全部成功,要么全部失败。

继续按照上面的例子可以简单的理解为EF Core是仓储、SaveChanges是工作单元(要么这一波全部成功,要么全部失败)。

总结:

因为领域服务不依赖外部系统、不保存状态,所以领域服务比应用服务更容易进行单元测试,这对于提高系统的质量是非常有帮助的。

DDD之领域事件、集成事件(十分重要)

事务脚本处理“事件”

1、“当发生某事件的时候,执行某个动作”。
2、当有人回复了用户的提问的时候,系统就向提问者的邮箱发送通知邮件。事务脚本的实现:

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

1、代码会随着需求的增加而持续膨胀。比如增加功能“如果用户回复的答案中有涉嫌违法的内容,则先把答案隐藏,并且通知审核人员进行审核”。怎么做?

2、代码可扩展性低。比如把“发送邮件”改成“发送短信”,怎么办?
“开闭原则”:对扩展开放,对修改关闭。

3、容错性差。外部系统并不总是稳定的。

//1、
void 保存答案(long id,string answer)
{
	保存到数据库(id,answer);
    if(答案违规)//一般是调用第三方鉴黄服务
    {
        hide();
    }
    else
    {
        string email = 获取提问者邮箱(id);
		发送邮件(email,"你的问题被回答了");//调用第三方邮箱服务 可能会挂掉
    }
}
//2、
void 保存答案(long id,string answer)
{
	保存到数据库(id,answer);
    if(答案违规)//一般是调用第三方鉴黄服务 可能会挂掉
    {
        hide();
    }
    else
    {
        string phoneNo = 获取提问者手机(id);
		发送短信(phoneNo,"你的问题被回答了");//调用第三方邮箱服务 可能会挂掉
    }
}

以上代码并不满足==“开闭原则”:对扩展开放,对修改关闭。==

采用事件机制的伪代码

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,"你的问题被回答了");
}

优点:关注点分离;容易扩展;容错性好;

上诉代码如果需要保存数据,刷新缓存。则加以下代码即可,并不需要修改原有代码

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

两种事件

1、DDD中的事件分为两种类型:领域事件(Domain Events)和集成事件(Integration Events)。
2、领域事件:在同一个微服务内的聚合之间的事件传递。使用进程内的通信机制完成。
3、集成事件:跨微服务的事件传递。使用事件总线(EventBus)实现。

总结

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

集成事件由于需要跨微服务进行通信,我们就要引入事件总线(eventbus)来实现事件的传递。我们一般使用消息队列服务器中的“发布/订阅”模式来实现事件总线。

本文内容大部分都为杨中科老师《ASP.NET Core技术内幕与项目实战》一书中内容,此文只是做学习记录,如有侵权,联系立马删除。