- 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分为 战略设计 和 战术设计 ,战略设计是一种宏观的顶层设计,而战术设计则更偏向于代码落地实践.
战略设计中有通用语言、领域、子域和限界上下文等概念,这些概念不好理解得清楚,也不好讲得清楚。但是,从本质上讲, DDD的战略设计只在解决一个问题,即软件的模块化划分的问题 。为此,我们将在下一篇 战略设计 中进行详细阐述.
当我们把软件的模块划分好(也即完成了战略设计)之后,下一步自然是编码实现了,于是乎我们也就顺理成章地进入了DDD的战术设计范畴。DDD的战术设计包括聚合根、实体和资源库等众多概念,其中最重要的当属聚合根了,那接下来我们就从聚合根讲起.
在上一篇 DDD入门 中我们提到,DDD的一个重要使命是实现软件中“业务复杂度”和“技术复杂度”的分离。为此,我们使用 领域模型 来描述业务,使之与数据库、消息队列等技术实现解耦开来。在DDD的领域模型中,最核心的则是 聚合根 (Aggregate Root),我们甚至可以认为整个DDD都是围绕聚合根的设计与实现展开的.
聚合根中的“聚合”即“高内聚,低耦合”中的“内聚”之意,而“根”则是“根部”的意思。事实上,并不存在一个教科书式的对聚合根的理论定义,你可以将聚合根理解为一个系统中最重要的那些名词。为了给你一个直观的理解,我们先来看看以下聚合根的例子:
怎么样,是不是至少对聚合根有了一个感性认识?这些名词是其所在的软件系统之所以存在的原因,设想淘宝中没有了订单的概念还能称之为电商系统吗?当然,一个概念是否能成为聚合根是根据其所处的业务场景而定的,我们将在后续文章 聚合根和资源库 中做详细解释.
聚合根是业务逻辑的主要载体,在理想情况下应该是业务的唯一载体。但是,万事皆有但是,有时将业务逻辑放到聚合根中是不合适的,甚至是不可行的。比如,在 码如云 中,在更新成员手机号时,需要先行检查该手机号是否已经被占用,此时成员聚合根自身并不具备检查其他成员手机号的功能,因此要将这部分逻辑放到成员本身上则显得不合适了,我们在上一篇 DDD入门 中也提到了这个例子。不过不要慌,针对这种情形,DDD给出了专门的概念 —— 领域服务 (Domain Service)。领域服务是聚合根自身无法完成业务逻辑时的代替品,是不得已而为之的一个概念,通常用于处理一些跨聚合操作或者需要访问技术基础设施的场景.
在下例中,领域服务 MemberDomainService 中的 changeMyMobile() 方法实现了两处聚合根(Member)无法自身实现的功能:一是调用 mryPasswordEncoder 检查密码是否正确,二是调用 memberRepository 检查手机号是否重复.
//领域服务:MemberDomainService
public void changeMyMobile(Member member, String newMobile, String password) {
if (!mryPasswordEncoder.matches(password, member.getPassword())) {
throw new MryException(PASSWORD_NOT_MATCH,
"修改手机号失败,密码不正确。", "memberId", member.getId());
}
if (Objects.equals(member.getMobile(), newMobile)) {
return;
}
if (memberRepository.existsByMobile(newMobile)) {
throw new MryException(MEMBER_WITH_MOBILE_ALREADY_EXISTS,
"修改手机号失败,手机号对应成员已存在。",
mapOf("mobile", newMobile,
"memberId", member.getId()));
}
member.changeMobile(newMobile, member.toUser());
}
源码出处: com/mryqr/core/member/domain/MemberDomainService.java 。
需要提醒的是,请不要被领域服务名字中的“服务”迷惑了,领域服务依然是领域模型的一部分,因为它也实现了业务逻辑,更多关于领域服务的内容,请查看本系列的 应用服务和领域服务 一文.
从更广义上讲,聚合根属于实体的范畴。在DDD中,存在 实体 (Entity)和 值对象 (Value Object)是一对相互对立的概念,实体用于表示那些具有生命周期的“存在”,而值对象用于表示那些仅仅起描述性作用的东西。实体通过唯一标识进行标定,而值对象则通过其包含的所有属性进行标定。在编码实现时,最直观的区别则是实体对象有ID,而值对象没有ID;此外,实体对象一般包含比较复杂的业务逻辑,而值对象通常则是一些简单的小对象,业务逻辑相对简单。举个常见的例子,无论一对双胞胎长得多么的相像,但由于两个人的身份证号不同(即ID不同),那么两人便属于不同的实体;而对于货币来说,一张崭新的百元大钞和一张破旧的占满了细菌的百元大钞是可以等价交换的,因为他们所包含的属性值(均是100元)是一样,因此他们均属于值对象.
在码如云中,管理员可以对表单提交(Submission)进行审批(Approval),审批结果存放在 SubmissionApproval 对象中,该对象则是一个值对象:
@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class SubmissionApproval {
private final boolean passed;//审批是否通过
private final String note;//审批意见
private final Instant approvedAt;//审批时间
private final String approvedBy;//审批人ID
}
源码出处: com/mryqr/core/submission/domain/SubmissionApproval.java 。
需要注意的是,虽然聚合根属于实体,但是实体却不只是包含聚合根。事实上,聚合根隶属于实体,同时其内部又可以包含其他实体。举个例子,汽车作为聚合根是一个实体,同时汽车内部的发动机也是一个实体,但发动机却不是聚合根。在本系列的 实体和值对象 中,我们将详细介绍实体和值对象的区别.
有些对象(特别是聚合根)的创建过程本身也是业务逻辑的一部分,在DDD中,为了显式化业务逻辑,也为了遵从关注点分离的原则,我们将这些对象的构建过程封装到 工厂 (Factory)中,落地时可以是独立的工厂类,也可以是一个对象中的工厂方法。在码如云中,所有的聚合根对象均配备有专门的工厂类,比如用于创建成员的 MemberFactory 如下:
@Component
@RequiredArgsConstructor
public class MemberFactory {
private final MemberRepository memberRepository;
private final DepartmentRepository departmentRepository;
public Member create(String name,
List<String> departmentIds,
String mobile,
String email,
String password,
User user) {
//此处只为展示工厂类,省略了具体实现细节
return create(name, departmentIds, mobile, email, password, null, user);
}
}
源码出处: com/mryqr/core/member/domain/MemberFactory.java 。
更多关于工厂的讲解,请参考本系列的 实体与值对象 一文.
在DDD的领域模型中,一个业务操作通常会导致一个结果,这个结果被称为 领域事件 ,即领域模型中已经发生的事情,比如“成员手机号已更新”便是一个领域事件。领域事件通常用于组件之间的因果关系处理,比如当“成员手机号已更新”事件产生后,我们可能会在另一个业务组件中做相应的同步操作,这里的组件粒度可以是聚合根,可以是其他业务模块,还可以是一个独立的第三方系统.
在码如云中,创建成员将产生“成员已创建”事件 MemberCreatedEvent :
@Getter
@TypeAlias("MEMBER_CREATED_EVENT")
@NoArgsConstructor(access = PRIVATE)
public class MemberCreatedEvent extends DomainEvent {
private String memberId;
public MemberCreatedEvent(String memberId, User user) {
super(MEMBER_CREATED, user);
this.memberId = memberId;
}
}
源码出处: com/mryqr/core/member/domain/event/MemberCreatedEvent.java 。
更过关于领域事件的讲解,请参考本系列的 领域事件 一文.
以上的聚合根、实体、值对象、工厂、领域服务和领域事件都是针对领域模型而言的,虽然在DDD中领域模型是当之无愧的大哥大,但是在实际的软件系统中,单单有领域模型是无法正常运作的,还需要有围绕着领域模型的其他周边设施,为此DDD给出了资源库和应用服务等概念.
简单地讲, 资源库 (Repository)是用于保存/获取聚合根的。在此之前你可能了解过 DAO 对象也是用于存储对象的,但是与DAO不同的是,资源库操作的基本单位是聚合根,也即只有聚合根对象才配得上拥有资源库,其他实体对象则没有.
在码如云中,成员对象对应的资源库 MemberRepository 接口定义如下:
public interface MemberRepository {
Member byId(String id);
boolean exists(String arId);
void save(Member member);
void delete(Member member);
//...此处省略其他方法
}
源码出处: com/mryqr/core/member/domain/MemberRepository.java 。
一般来说,资源库分为接口类和实现类,接口类属于领域模型,实现类属于基础设施。这样的好处是,领域模型只依赖于接口,而不依赖于基础设施,有利于维持领域模型的技术中立性。更过关于资源库的讲解,请查看本系列的 聚合根和资源库 一文.
领域模型是用来完成业务功能的,也即需要响应用户发起的各种请求,但是在软件系统中在这些请求到达领域模型之前,事实上还有很多事情需要处理,比如需要从数据库中加载数据(聚合根)、处理事务、权限管控等,在DDD中,这些操作由 应用服务 (Application Service)完成。应用服务可以看做是领域模型的门面,它将接收到请求派发给合适的领域模型去处理,在整个过程中,应用服务充当的是协调者和编排者的角色,就像酒店的前台一样.
在码如云中,每一个聚合根都有对应的应用服务,比如对于成员来说,应用服务 MemberCommandService 如下:
//由于应用服务的"Application"与码如云中的应用聚合根"App"重名,在码如云中,使用“CommandService”来表示应用服务,以示区分
@Slf4j
@Component
@RequiredArgsConstructor
public class MemberCommandService {
//......
@Transactional
public void changeMyMobile(ChangeMyMobileCommand command, User user) {
mryRateLimiter.applyFor(user.getTenantId(), "Member:ChangeMyMobile", 5);
String mobile = command.getMobile();
verificationCodeChecker.check(mobile, command.getVerification(), CHANGE_MOBILE);
Member member = memberRepository.byId(user.getMemberId());
memberDomainService.changeMyMobile(member, mobile, command.getPassword());
memberRepository.save(member);
log.info("Mobile changed by member[{}].", member.getId());
}
//......
}
源码出处: com/mryqr/core/member/command/MemberCommandService.java 。
在本系列的 应用服务与领域服务 一文中,我们将对应用服务做详细讲解.
以上,我们概览式地了解了DDD战略设计和战术设计中的各种主要概念,在本系列的后续文章中,我们将针对这些概念进行逐一讲解.
最后此篇关于产品代码都给你看了,可别再说不会DDD(二):DDD概念大白话的文章就讲到这里了,如果你想了解更多关于产品代码都给你看了,可别再说不会DDD(二):DDD概念大白话的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
关闭。这个问题是off-topic .它目前不接受答案。 想改进这个问题? Update the question所以它是on-topic对于堆栈溢出。 10年前关闭。 Improve this qu
我正在开发一个 Android 应用程序。在此应用程序中, Logo 栏显示在所有页面( Activity )上,或者我们可以说它在所有页面上都有标题。这个 Logo 栏有几个图标,如主页、登录、通知
我正在使用 hadoop 使用开源接口(interface) HVPI 处理视频。然而,inputsplit 的实现,更准确地说是在 isSplitableobContext (context, Pa
1. 是什么? MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System
有没有办法使用 c++20s 的概念来检查一个值是否满足某些要求? 假设我正在编写某种使用分页的容器,并且我想让页面大小成为模板参数。 template class container; 我可以使用带
如何在 ArrayList 中循环遍历 ArrayList? 例如,如果我有一个名为 Plants of Plant 对象的 ArrayList。每个 Plant 对象内部都有一个随机数量的花名。我如
如何在UML类图中绘制C++概念? 具体来说,我有以下代码: template concept Printable = requires(T a, std::ostream &where) {
我有兴趣制作一个网站,在访问者访问时闪现整个网络历史记录。我计划使用 JavaScript 来获取每个观看者计算机上的历史记录,并根据他们拥有的内容以不同的速度对其进行动画处理。我的想法是使用 his
有一个模板定义,例如: template void foo( void ) { /* ... */ } 如何定义一个概念,以便N必须为非零正值(N> = 1)? 就像是: template con
封装是信息隐藏还是导致信息隐藏? 正如我们所说,封装将数据和函数绑定(bind)在单个实体中,因此它为我们提供了对数据流的控制,并且我们只能通过一些定义良好的函数来访问实体的数据。因此,当我们说封装导
下面有一个简单的代码片段,它使用以下方式进行编译: g++-9 -std=c++2a -fconcepts 这是试图定义一个需要存在函数的概念。我希望输出是"is",但事实并非如此……知道为什么吗?谢
我有一个普通二元运算符的概念 template concept is_binary_operation = requires (const T& t1, const T& t2) // e.g
我正在c++ 20中实现具有启发式功能的搜索算法。 我试图用类似这样的概念来约束我的算法可以使用的功能: template concept Heuristic = requires(SelfType
我需要了解 SAS 如何读取/执行数据步骤。当我查找有关 SAS 如何读取数据步骤的信息时,我似乎只找到有关它如何读取以进行合并的信息,我不了解与常规数据步骤相关的信息。比方说,我有这行代码: dat
最近我看到一个关于“框架”的问题,如果“框架”有不同的类型或概念。那么,存在不同“类型”的“框架”吗? 例如:NodeJS 是一种“类型”(概念),而 Hibernate ORM 是另一种“类型”(概
如何使用任何技术禁用或清除客户端浏览器 Cookie 我认为使用 javascript 可以用于任何技术 最佳答案 var cookies = document.cookie.split(";");
我正在使用 target = "_blank" 单击链接时生成新选项卡。但是,浏览器会将焦点移至该选项卡。 有没有办法让焦点保持在当前标签页上? 回答摘要 基本上,只需发送一个模拟控件点击的当前事件。
我正在尝试在我的 android/firebase(cloud firestore) 应用程序上添加一项需要其他用户批准/拒绝的功能。例如,当 Air&BnB 上的用户想要预订一个地方时,所有者必须批
这个问题在这里已经有了答案: mysql_fetch_array()/mysql_fetch_assoc()/mysql_fetch_row()/mysql_num_rows etc... expec
public class MyClass { public static void main(String[] args) { System.out.println("Hell
我是一名优秀的程序员,十分优秀!