- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
这是一个讲解DDD落地的文章系列,作者是《实现领域驱动设计》的译者 滕云 。本文章系列以一个真实的并已成功上线的软件项目—— 码如云 ( https://www.mryqr.com )为例,系统性地讲解DDD在落地实施过程中的各种典型实践,以及在面临实际业务场景时的诸多取舍.
本系列包含以下文章:
既然DDD是“领域”驱动,那么我们便不能抛开业务而只讲技术,为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云 (不是马云,也不是码云)。如你已经在本系列的其他文章中了解过该案例,可跳过.
码如云 是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,并以该二维码为入口展开对“物品”的相关操作,典型的应用场景包括固定资产管理、设备巡检以及物品标签等.
在使用码如云时,首先需要创建一个 应用 (App),一个 应用 包含了多个 页面 (Page),也可称为表单,一个 页面 又可以包含多个 控件 (Control),比如单选框控件。 应用 创建好后,可在 应用 下创建多个 实例 (QR)用于表示被管理的对象(比如机器设备)。每个 实例 均对应一个二维码,手机扫码便可对 实例 进行相应操作,比如查看 实例 相关信息或者填写页面表单等,对表单的一次填写称为 提交 (Submission);更多概念请参考 码如云术语 .
在技术上,码如云是一个无代码平台,包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发,其后端技术栈主要有Java、Spring Boot和MongoDB等.
码如云的源代码是开源的,可以通过以下方式访问:
码如云源代码: https://github.com/mryqr-com/mry-backend 。
在上一篇 代码工程结构 中,我们从宏观层面讲到了DDD项目的目录结构,但并未触及到实际的代码。在本文中,我们将深入到代码中,逐一讲解DDD中对各种请求类型的典型处理流程.
在本系列的 DDD概念大白话 我们提到,DDD中的所有组件都是围绕着 聚合根 展开的,其中有些本身即是聚合根的一部分,比如实体和值对象;有些是聚合根的客户,比如应用服务;有些则是对聚合根的辅助或补充,比如领域服务和工厂。反观当下流行的各种软件架构,无论是分层架构、六边形架构还是整洁架构,它们都有一个共同点,即在架构中心都有一个核心存在,这个核心正是领域模型,而DDD的聚合根则存在于领域模型之中.
不难看出,既然每种架构中都有为领域模型预留的位置,这也意味着DDD可采用任何一种软件架构。事实也的确如此,DDD并不要求采用哪种特定架构,如果你真要说DDD项目应该采用某种架构的话,那么应该“以领域模型为中心的软件架构”.
如果我们把软件系统当做一个黑盒的话,其外界是各种形态的客户端,比如浏览器,手机APP或者第三方调用方等,盒子内部则是我们精心构建的领域模型。不过,领域模型是不能直接被外界访问的,主要原因有以下两点:
接下来,让我们来看看DDD项目是如何衔接外部请求和内部领域模型的。既然聚合根是领域模型中的一等公民,那么按照对聚合根的操作类型不同,DDD项目中主要存在以下4种类型的请求:
咋一看,你可能会说这不就是CRUD么?本质上这的确是CRUD,但是这里的CRUD可不是仅仅操作数据库那么简单,你如果阅览过本系列的上一篇 代码工程结构 的话,便知道在 码如云 中领域模型的代码量占比远远高出数据库访问相关的代码量.
本文主要讲解DDD对请求的处理流程,并不讲解聚合根本身的设计和实现,而是假设聚合根(以及领域模型中的工厂和领域服务等)已经实现就位了,关于聚合根本身的讲解请参考本系列的 聚合根与资源库 一文。此外,为了突出重点,本文只着重讲解请求处理流程的主干,而忽略与之关系不大的其他细节,比如我们将忽略应用服务中的事务处理和权限管理等功能,为此读者可参考 应用服务与领域服务 .
聚合根的创建通常通过 工厂 类完成,请求流经路线为:控制器(Controller) -> 应用服务(Application Service) -> 工厂(Factory) -> 资源库(Repository).
在码如云中,当用户提交表单后,系统后台将创建一份提交(Submission),这里的 Submission 便是一个聚合根对象。在整个“创建Submission”的处理流程中,请求先通过HTTP协议到达Spring MVC中的Controller:
//SubmissionController
@PostMapping
@ResponseStatus(CREATED)
public ReturnId newSubmission(@RequestBody @Valid NewSubmissionCommand command,
@AuthenticationPrincipal User user) {
String submissionId = submissionCommandService.newSubmission(command, user);
return returnId(submissionId);
}
源码出处: com/mryqr/core/submission/SubmissionController.java 。
Controller的作用只是为了衔接技术和业务,因此其逻辑应该相对简单,在本例中, SubmissionController 的 newSubmission() 方法仅仅将请求代理给应用服务 SubmissionCommandService 即完成了其自身的使命。这里的 NewSubmissionCommand 表示命令对象,用于携带请求数据,比如对于“创建Submission”来说, NewSubmissionCommand 对象中至少应该包含表单的提交内容等数据。命令对象是外部客户端传入的数据,因此需要将其与领域模型解耦,也即命令对象不能进入到领域模型的内部,其所能到达的最后一站是应用服务.
处理流程的下一站是应用服务,应用服务是整个领域模型的门面,无论什么类型的客户端,只要业务用例相同,那么所调用的应用服务的方法也应相同,也即应用服务和技术设施也是解耦的.
//SubmissionCommandService
@Transactional
public String newSubmission(NewSubmissionCommand command, User user) {
AppedQr appedQr = qrRepository.appedQrById(command.getQrId());
App app = appedQr.getApp();
QR qr = appedQr.getQr();
Page page = app.pageById(command.getPageId());
SubmissionPermissions permissions = permissionChecker.permissionsFor(user, appedQr);
permissions.checkPermissions(app.requiredPermission(), page.requiredPermission());
Set<Answer> answers = command.getAnswers();
Submission submission = submissionFactory.createNewSubmission(
answers,
qr,
page,
app,
permissions.getPermissions(),
command.getReferenceData(),
user
);
submissionRepository.houseKeepSave(submission, app);
log.info("Created submission[{}].", submission.getId());
return submission.getId();
}
源码出处: com/mryqr/core/submission/command/SubmissionCommandService.java 。
在以上的 SubmissionCommandService 应用服务中,首先做权限检查,然后调用工厂 SubmissionFactory.createNewSubmission() 完成 Submission 的创建,最后调用资源库 SubmissionRepository.houseKeepSave() 将新建的 Submission 持久化到数据库中。从中可见,应用服务主要用于协调各方以完成一个业务用例,其本身并不包含业务逻辑,业务逻辑在工厂中完成.
//SubmissionFactory
public Submission createNewSubmission(Set<Answer> answers,
QR qr,
Page page,
App app,
Set<Permission> permissions,
String referenceData,
User user) {
if (page.isOncePerInstanceSubmitType()) {
submissionRepository.lastInstanceSubmission(qr.getId(), page.getId())
.ifPresent(submission -> {
throw new MryException(SUBMISSION_ALREADY_EXISTS_FOR_INSTANCE,
"当前页面不支持重复提交,请尝试更新已有表单。",
mapOf("qrId", qr.getId(),
"pageId", page.getId()));
});
}
//...此处忽略更多业务逻辑
//只有需要登录的页面才记录user
User finalUser = page.requireLogin() ? user : ANONYMOUS_USER;
Map<String, Answer> checkedAnswers = submissionDomainService.checkAnswers(answers,
qr,
page,
app,
permissions);
return new Submission(checkedAnswers,
page.getId(),
qr, app,
referenceData,
finalUser);
}
源码出处: com/mryqr/core/submission/domain/SubmissionFactory.java 。
虽然工厂用于创建聚合根,但并不是直接调用聚合根的构造函数那么简单,从 SubmissionFactory.createNewSubmission() 可以看出,在创建 Submission 之前,需要根据表单类型检查是否可以创建新的 Submission ,而这正是业务逻辑的一部分。因此,工厂也属于领域模型的一部分,本质上工厂可以认为是一种特殊形式的领域服务.
请求流程的最后,应用服务调用资源库 submissionRepository.houseKeepSave() 完成对新建 Submission 的持久化。更多关于资源库的内容,请参考 聚合根与资源库 一文.
对聚合根的更新流程通常可以通过“经典三部曲”完成:
此时的请求流经路线为:控制器(Controller) -> 应用服务(Application Service) -> 资源库(Repository) -> 聚合根(Aggregate Root).
在 码如云 中,当表单开启了审批功能过后,管理员可对 Submission 进行审批操作,本质上则是在更新 Submission 。在“审批Submission”的过程中,请求依然是首先到达Controller:
//SubmissionController
@ResponseStatus(CREATED)
@PostMapping(value = "/{submissionId}/approval")
public ReturnId approveSubmission(@PathVariable("submissionId") @SubmissionId @NotBlank String submissionId,
@RequestBody @Valid ApproveSubmissionCommand command,
@AuthenticationPrincipal User user) {
submissionCommandService.approveSubmission(submissionId, command, user);
return returnId(submissionId);
}
源码出处: com/mryqr/core/submission/SubmissionController.java 。
与“创建聚合根”相似, SubmissionController 直接将请求代理给应用服务 SubmissionCommandService.approveSubmission() :
//SubmissionCommandService
@Transactional
public void approveSubmission(String submissionId,
ApproveSubmissionCommand command,
User user) {
Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);
App app = appRepository.cachedById(submission.getAppId());
Page page = app.pageById(submission.getPageId());
SubmissionPermissions permissions = permissionChecker.permissionsFor(user,
app,
submission.getGroupId());
permissions.checkCanApproveSubmission(submission, page, app);
submission.approve(command.isPassed(),
command.getNote(),
page,
user);
submissionRepository.houseKeepSave(submission, app);
log.info("Approved submission[{}].", submissionId);
}
源码出处: com/mryqr/core/submission/command/SubmissionCommandService.java 。
应用服务 SubmissionCommandService 先通过资源库 SubmissionRepository 的 byIdAndCheckTenantShip() 方法获取到需要操作的 Submission ,然后进行权限检查,再调用 Submission.approve() 方法完成对 Submission 的更新,最后调用资源库 SubmissionRepository 的 houseKeepSave() 方法将更新后的 Submission 保存到数据库。这里的重点在于:需要保证所有的业务逻辑均放在 Submission.approve() 中:
//Submission
public void approve(boolean passed,
String note,
Page page,
User user) {
if (isApproved()) {
throw new MryException(SUBMISSION_ALREADY_APPROVED,
"无法完成审批,先前已经完成审批。",
"submissionId", this.getId());
}
this.approval = SubmissionApproval.builder()
.passed(passed)
.note(note)
.approvedAt(now())
.approvedBy(user.getMemberId())
.build();
raiseEvent(new SubmissionApprovedEvent(this.getId(),
this.getQrId(),
this.getAppId(),
this.getPageId(),
this.approval,
user));
addOpsLog(passed ?
"审批" + page.approvalPassText() :
"审批" + page.approvalNotPassText(), user);
}
源码出处: com/mryqr/core/submission/domain/Submission.java 。
可以看到, Submission.approve() 先检查 Submission 是否已经被审批过了,如果尚未审批才继续审批操作,审批过程还会发出“提交已审批”( SubmissionApprovedEvent )领域事件(更多关于领域事件的内容,请参考本系列的 领域事件 一文)。 Submission.approve() 中的代码量虽然不多,但是却体现了核心的业务逻辑:“已经完成审批的提交不能再次审批”.
当然,并不是所有的业务用例都适合“经典三部曲”,有时聚合根自身无法完成所有的业务逻辑,此时我们则需要借助领域服务(Domain Service)来完成请求的处理。比如,常见的使用领域服务的场景是需要进行跨聚合查询的时候。此时的请求流经路线则为:控制器(Controller) -> 应用服务(Application Service) -> 资源库(Repository) -> 聚合根(Aggregate Root) ->领域服务(Domain Service).
在码如云中,管理员可以对既有的 Submission 进行编辑更新,但是由于更新时可能涉及到检查手机号或者邮箱等控件填值的唯一性,因此在更新时需要跨 Submission 进行查询,此时光靠 Submission 自身便无法完成了,为此我们可以创建领域服务 SubmissionDomainService 用于跨 Submission 操作:
//SubmissionCommandService
@Transactional
public void updateSubmission(String submissionId,
UpdateSubmissionCommand command,
User user) {
Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);
AppedQr appedQr = qrRepository.appedQrById(submission.getQrId());
App app = appedQr.getApp();
QR qr = appedQr.getQr();
Page page = app.pageById(submission.getPageId());
SubmissionPermissions permissions = submissionPermissionChecker.permissionsFor(user,
app,
submission.getGroupId());
permissions.checkCanUpdateSubmission(submission, page, app);
submissionDomainService.updateSubmission(submission,
app,
page,
qr,
command.getAnswers(),
permissions.getPermissions(),
user
);
submissionRepository.houseKeepSave(submission, app);
log.info("Updated submission[{}].", submissionId);
}
源码出处: com/mryqr/core/submission/command/SubmissionCommandService.java 。
在本例中,应用服务 SubmissionCommandService 并未直接调用聚合根 Submission 中的方法,而是将 Submission 作为参数传入了领域服务 SubmissionDomainService 的 updateSubmission() 方法中,在 SubmissionDomainService 完成了对 Submission 的更新后, SubmissionCommandService 再调用 SubmissionRepository.houseKeepSave() 方法将 Submission 保存到数据库中。 SubmissionDomainService.updateSubmission() 实现如下:
//SubmissionDomainService
public void updateSubmission(Submission submission,
App app,
Page page,
QR qr,
Set<Answer> answers,
Set<Permission> permissions,
User user) {
Map<String, Answer> checkedAnswers = checkAnswers(answers,
qr,
page,
app,
submission.getId(),
permissions);
Set<String> submittedControlIds = answers.stream()
.map(Answer::getControlId)
.collect(toImmutableSet());
submission.update(submittedControlIds, checkedAnswers, user);
}
源码出处: com/mryqr/core/submission/domain/answer/SubmissionDomainService.java 。
可以看到, SubmissionDomainService.updateSubmission() 首先调用业务方法 checkAnswers() 对表单内容进行检查(其中便包含上文提到的对手机号或邮箱的重复性检查),再调用 Submission.update() 以完成对 Submission 的更新,相当于 SubmissionDomainService 对 Submission 做了业务上的加工.
这里,领域服务 SubmissionDomainService 的职责范围仅包含对聚合根 Submission 的更新,并不负责持久化 Submission ,持久化的职责依然在应用服务 SubmissionCommandService 上。这种方式的好处在于:(1)与“经典三部曲”保持一致,将所有持久化操作均集中到应用服务中,不至于过于分散;(2)使领域服务的职责尽量单一.
聚合根删除流程相对简单,此时的请求流经路线为:控制器(Controller) -> 应用服务(Application Service) -> 资源库(Application Service) -> 聚合根(Aggregate Root) .
删除请求首先到达Controller:
//SubmissionController
@DeleteMapping(value = "/{submissionId}")
public ReturnId deleteSubmission(@PathVariable("submissionId") @SubmissionId @NotBlank String submissionId,
@AuthenticationPrincipal User user) {
submissionCommandService.deleteSubmission(submissionId, user);
return returnId(submissionId);
}
源码出处: com/mryqr/core/submission/SubmissionController.java 。
Controller将请求进一步代理给应用服务 SubmissionCommandService :
//SubmissionCommandService
@Transactional
public void deleteSubmission(String submissionId, User user) {
Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);
Group group = groupRepository.cachedById(submission.getGroupId());
managePermissionChecker.checkCanManageGroup(user, group);
submission.onDelete(user);
submissionRepository.delete(submission);
log.info("Deleted submission[{}].", submissionId);
}
源码出处: com/mryqr/core/submission/command/SubmissionCommandService.java 。
应用服务 SubmissionCommandService 通过 SubmissionRepository 加载出需要删除的 Submission 后,再调用 Submission.onDelete() 以完成删除前的一些操作,在本例中 onDelete() 将发出“提交已删除”( SubmissionDeletedEvent )领域事件:
//Submission
public void onDelete(User user) {
raiseEvent(new SubmissionDeletedEvent(this.getId(),
this.getQrId(),
this.getAppId(),
this.getPageId(),
user));
}
源码出处: com/mryqr/core/submission/domain/Submission.java 。
最后,应用服务 SubmissionCommandService 调用 SubmissionRepository.delete() 完成对聚合根的删除操作.
在本系列的 CQRS 一文中,我们将专门讲到在DDD中如何做查询操作.
在本文中,我们分别对聚合根的新建、更新和删除的典型请求处理流程做了详细介绍。在这些流程中,我们以聚合根为中心,围绕之形成了恰如其分的软件架构。在下一篇 聚合根与资源库 中,我们将对聚合根本身的设计与实现做详细讲解.
最后此篇关于产品代码都给你看了,可别再说不会DDD(五):请求处理流程的文章就讲到这里了,如果你想了解更多关于产品代码都给你看了,可别再说不会DDD(五):请求处理流程的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
这里有一个很大的设计缺陷,但我无法解决它: 业务需要有点涉及,所以我会尽量保持简单。 我们有一张购物表和一张退货表。当进行退货时,我们必须找到与数据库中最早购买的退货匹配,并将其记录在“已应用退货”表
在我的家庭项目中,我遇到了确定域对象类型的问题。 领域:公交时刻表 限界上下文:路由(公共(public)交通基础设施,ctx1)、时间表(调度,ctx2) 对象: Station - 描述一个公交车
我有一个名为“产品类型”的值类型,它被分配给了一个产品。 (一个产品有一个产品类型) 为了允许用户从列表中选择类型,我将填充一个下拉列表。在哪里检索产品类型列表最合适?一个实现存储库模式的类? 编辑:
域这个词在 DDD 中究竟是什么意思?我一直在阅读定义……虽然我看到了域模型之类的东西并理解模型是什么 - 域模型是什么意思? 域实际上是什么意思? 谢谢 最佳答案 域是指您的应用程序解决的主题。 例
DDD 中的域模型应该与持久性无关。 CQRS 要求我为我不想在读取模型中包含的所有内容触发事件。 (顺便说一下,将我的模型拆分为一个写模型和至少一个读模型)。 ES 要求我为所有改变状态的事件触发事
Eric 在他的书中触及了 的主题。模块 很少。他也没有通过示例讨论模块结构与有界上下文的关系。限界上下文是否包含模块或模块包含限界上下文?当应用程序具有 DDD 时,它的可扩展性有多容易? 在我们设
前言 笔者于2021年入职了杭州一家做水务系统的公司,按照部门经理要求,新人需要做一次个人分享(主题随意)。 当时笔者对DDD充满了浓厚的兴趣,之前也牛刀小试过,于是就决定班门弄斧Show一下
上部分 模型驱动设计的构造块 为维护模型和实现之间的关系打下了基础。在开发过程中使用一系列成熟的基本构造块并运用一致的语言,能够使开发工作更加清晰而有条理。 我们面临的真正挑战是找到
3. 领域对象的生命周期 。 每个对象都有生命周期,如下图所示。对象自创建后,可能会经历各种不同的状态,直至最终消亡——要么存档,要么删除。当然很多对象是简单的临时对象,仅通过调用构造函数来创建
为了保证软件实践得简洁并且与模型保持一致,不管实际情况如何复杂,必须运用建模和设计的实践. 某些设计决策能够使模型和程序紧密结合在一起,互相促进对方的效用。这种结合要求我们注意每个元素的细节
大家好,我是 ddd 设计的新手,正在尝试使用这种在 C# 中工作的模式开发我的第一个应用程序 在我的应用程序中,我有一个包含子实体 Assets 的聚合合约,当添加或结算 Assets 时,我应该在
我正在尝试弄清楚如何使项目的一些消费者(业务客户)的不变量保持一致,他们对同一版本的聚合根有自己的要求。让我们以客户为例,提出假设性问题以满足以下愚蠢的逻辑: public class Custome
我见过一些具有实体值对象表示的 DDD 项目。它们通常显示为 EmployeeDetail、EmployeeDescriptor、EmployeeRecord 等。有时它包含实体 ID,有时不包含。
我试图了解如何表示某些 DDD(域驱动设计)规则。 遵循蓝皮书约定,我们有: 根实体具有全局身份并负责检查不变量。 根实体控制访问,并且不会被其内部结构的更改所蒙蔽。 对内部成员的 transient
我对 ddd 中的验证方法有疑问。我已经阅读了相当有争议的意见。有人说这应该在实体之外,其他人说这应该放在实体中。我试图找到一种我可以遵循的方法。 例如,假设我有带有电子邮件和密码的 User 实体。
寻找有关如何解决此问题的建议,并了解域驱动设计是否真的是这里的最佳模式。 我的客户正在重新构建其几近过时的工具和服务堆栈。客户是一个快速扩张的电子商户。它的核心产品是它的大型电子商务网站。围绕该网站,
我很难找出实现依赖于数据库中存储的数据的业务规则验证的最佳方法。在下面的简化示例中,我想确保用户名属性是唯一的。 public class User() { public int Id { g
情况: 要处理域事件,Jimmy Bogart proposed 一种将事件存储在聚合中的方法。 在我看来,这是一种非常方便的方法。但是,域服务中的域事件怎么办? 域服务不应该有状态(stateles
我正在处理遗留项目,试图改进项目结构。我的问题是我应该如何组织代码结构。我看到两个选项: #1 business-domain / layer app/ ----accout/ --------app
根据 DDD 原则,所有处理与特定聚合根对象相关的实体的 CRUD 操作都应该由聚合根进行。 但是我们如何从 aggr 根中仅更改实体的单个属性?我们应该在实体中有 setter 方法吗?这些方法应该
我是一名优秀的程序员,十分优秀!