- 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的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我需要按不同类别过滤我网站上的产品。例如,如果您选择“DRINKS”类别,它将向我显示属于该类别的产品。 为了更好地解释自己。 我需要按类别过滤我网站的出版物 [产品],例如,在选择一个类别时说“饮料
我有 orders 集合,其中包含 products 集合。我将一些产品 ID 作为列表传递给该方法。我需要返回与任何匹配的产品列表input 列表中的 id。 我需要像这样使用 foreach 循环
我已经为临时分发进行存档好几个月了,但今天突然我无法这样做,因为“存档”菜单项已被禁用。我没有改变任何东西。我完成了该项目的配置设置,看起来没问题。 我的临时个人资料即将在 14 天后过期。这可能是问
我正在尝试找出产品和产品属性之间的关系。我有一个 product 表和一个 product_attributes 表。产品可以具有多种属性。我需要查询来查找具有一个特定属性和另一个属性之一的所有产品。
我正在使用 MySQL Workbench 创建 EER 图。 实现产品、类别和公司表之间关系的最佳方式是什么? 我正在考虑这种关系,但考虑到我想让公司的客户管理自己的产品/类别,这是最好的方式吗?如
我正在使用 itertools 包,并尝试在具有 900 个值的数组中创建 1、2 和 3 的所有可能组合,然后将其转换为 30 x 30 矩阵。我必须执行此操作的代码在下面并且工作正常。 for d
我有几个关于 Cartridge 启动器的问题: 我的产品不需要评级或发布日期。他们永远不会出售。一些产品是可下载的,因此“num_in_stock”不相关或本质上是无限制的。没有颜色选项,只有尺寸。
在 MySQL 中,存储产品价格(或一般货币)的首选列类型是什么?谷歌知道我经常使用 DECIMAL of FLOAT,但我想知道哪个更好。 我存储的价格范围是 0.01 到 25.00。当然更高的值
在软件开发过程中,尤其是在准备将新功能或修复后的版本上线之前,进行详尽的自测和上线前检查是至关重要的。以下是一个从多个维度综合考量的上线升级检查清单(Checklist),旨在帮助团队确保软件质量、稳
我正在创建一个闪购网站,并且我已经在主页和商店页面上根据日期范围显示产品。但我也想根据其他地方的日期范围显示产品,因此使用简码。 这是我的代码: function testt($meta_query)
可以在 WooCommerce 上批量创建产品吗?我正在使用 wp-cli Product 命令,但似乎我必须一个一个地创建。 'My product 1'), array('title'
我有一个带有数量和价格列的 excel 文件,我用它来为插件 WooCommerce Dynamic Pricing 创建必要的输出的定价规则。 我几乎想通了,但是 WooCommerce 进口商正在
我刚刚继承了一个woocommerce项目,我需要将主页更改为仅显示特定品牌。他们设置了 Product-Data => Attribute => pa_brand。 如果我打印 pa_brand 数
在插件中如何使用 wc_get_products() 获取产品。或者有其他方法可以做到吗? if ( in_array( 'woocommerce/woocommerce.php', apply_fi
我正在做一个无法从公司网络外部访问的内部网,他们希望在 Plone 中显示一些关于文件下载和最常查看的页面的不错的统计数据。 由于网络限制,我无法使用谷歌分析或任何类型的外部服务,那么是否有任何产品可
我正在就以下问题寻求建议: 保留哪些产品 key 属于哪个客户端的列表。例如,如果我的产品 key 为 8456-7894-4567-7894,则应该这样设计,以便将列表写入数据库而不是文件。 如何将
就目前而言,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引起辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the he
尝试将产品搜索栏添加到 Wordpress 管理栏后端,以进行 Woocommerce 产品搜索。它将位于后端管理菜单栏的顶部,这样无论您在后端的哪个位置,都可以搜索 woo 的产品。我很接近但在小绊
这让我抓狂.. 我正在尝试根据特定属性查询和输出 WooCommerce 产品。例如,我设置了一个名为 on 的属性,可能的值为 yes或 no . 我使用以下查询: $args = array(
我正在尝试从 Shopify 商店获取所有产品的 JSON。我一直在向 {STORE URL}/products.json 端点。但这最终只显示了商店提供的部分产品(很多,但不是全部)。当我将参数更改
我是一名优秀的程序员,十分优秀!