领域驱动设计之银行转账:Wow框架实战
银行账户转账案例
银行账户转账案例是一个经典的领域驱动设计(DDD)应用场景。接下来我们通过一个简单的银行账户转账案例,来了解如何使用 Wow 进行领域驱动设计以及服务开发。
银行转账流程
- 准备转账(Prepare): 用户发起转账请求,触发 Prepare 步骤。这个步骤会向源账户发送准备转账的请求。
- 校验余额(CheckBalance): 源账户在收到准备转账请求后,会执行校验余额的操作,确保账户有足够的余额进行转账。
- 锁定金额(LockAmount): 如果余额足够,源账户会锁定转账金额,防止其他操作干扰。
- 入账(Entry): 接着,转账流程进入到目标账户,执行入账操作。
- 确认转账(Confirm): 如果入账成功,确认转账;否则,执行解锁金额操作。
- 成功路径(Success): 如果一切顺利,完成转账流程。
- 失败路径(Fail): 如果入账失败,执行解锁金额操作,并处理失败情况。
运行案例
- 运行 TransferExampleServer.java
- 查看 Swagger-UI : http://localhost:8080/swagger-ui.html
- 执行 API 测试:Transfer.http
自动生成 API 端点
运行之后,访问 Swagger-UI : http://localhost:8080/swagger-ui.html 。
该 RESTful API 端点是由 Wow 自动生成的,无需手动编写。

模块划分
| 模块 | 说明 |
|---|---|
| example-transfer-api | API 层,定义聚合命令(Command)、领域事件(Domain Event)以及查询视图模型(Query View Model),这个模块充当了各个模块之间通信的“发布语言”。 |
| example-transfer-domain | 领域层,包含聚合根和业务约束的实现。聚合根:领域模型的入口点,负责协调领域对象的操作。业务约束:包括验证规则、领域事件的处理等。 |
| example-transfer-server | 宿主服务,应用程序的启动点。负责整合其他模块,并提供应用程序的入口。涉及配置依赖项、连接数据库、启动 API 服务 |
领域建模
账户聚合根
状态聚合根(AccountState)与命令聚合根(Account)分离设计保证了在执行命令过程中,不会修改状态聚合根的状态。
状态聚合根(AccountState)建模
public class AccountState implements Identifier {
private final String id;
private String name;
/**
* 余额
*/
private long balanceAmount = 0L;
/**
* 已锁定金额
*/
private long lockedAmount = 0L;
/**
* 账号已冻结标记
*/
private boolean frozen = false;
@JsonCreator
public AccountState(@JsonProperty("id") String id) {
this.id = id;
}
@NotNull
@Override
public String getId() {
return id;
}
public String getName() {
return name;
}
public long getBalanceAmount() {
return balanceAmount;
}
public long getLockedAmount() {
return lockedAmount;
}
public boolean isFrozen() {
return frozen;
}
void onSourcing(AccountCreated accountCreated) {
this.name = accountCreated.name();
this.balanceAmount = accountCreated.balance();
}
void onSourcing(AmountLocked amountLocked) {
balanceAmount = balanceAmount - amountLocked.amount();
lockedAmount = lockedAmount + amountLocked.amount();
}
void onSourcing(AmountEntered amountEntered) {
balanceAmount = balanceAmount + amountEntered.amount();
}
void onSourcing(Confirmed confirmed) {
lockedAmount = lockedAmount - confirmed.amount();
}
void onSourcing(AmountUnlocked amountUnlocked) {
lockedAmount = lockedAmount - amountUnlocked.amount();
balanceAmount = balanceAmount + amountUnlocked.amount();
}
void onSourcing(AccountFrozen accountFrozen) {
this.frozen = true;
}
}
命令聚合根(Account)建模
@StaticTenantId
@AggregateRoot
public class Account {
private final AccountState state;
public Account(AccountState state) {
this.state = state;
}
AccountCreated onCommand(CreateAccount createAccount) {
return new AccountCreated(createAccount.name(), createAccount.balance());
}
@OnCommand(returns = {AmountLocked.class, Prepared.class})
List<?> onCommand(Prepare prepare) {
checkBalance(prepare.amount());
return List.of(new AmountLocked(prepare.amount()), new Prepared(prepare.to(), prepare.amount()));
}
private void checkBalance(long amount) {
if (state.isFrozen()) {
throw new IllegalStateException("账号已冻结无法转账.");
}
if (state.getBalanceAmount() < amount) {
throw new IllegalStateException("账号余额不足.");
}
}
Object onCommand(Entry entry) {
if (state.isFrozen()) {
return new EntryFailed(entry.sourceId(), entry.amount());
}
return new AmountEntered(entry.sourceId(), entry.amount());
}
Confirmed onCommand(Confirm confirm) {
return new Confirmed(confirm.amount());
}
AmountUnlocked onCommand(UnlockAmount unlockAmount) {
return new AmountUnlocked(unlockAmount.amount());
}
AccountFrozen onCommand(FreezeAccount freezeAccount) {
return new AccountFrozen(freezeAccount.reason());
}
}
转账流程管理器(TransferSaga)
转账流程管理器(TransferSaga)负责协调处理转账的事件,并生成相应的命令。
onEvent(Prepared): 订阅转账已准备就绪事件(Prepared),并生成入账命令(Entry)。onEvent(AmountEntered): 订阅转账已入账事件(AmountEntered),并生成确认转账命令(Confirm)。onEvent(EntryFailed): 订阅转账入账失败事件(EntryFailed),并生成解锁金额命令(UnlockAmount)。
@StatelessSaga
public class TransferSaga {
Entry onEvent(Prepared prepared, AggregateId aggregateId) {
return new Entry(prepared.to(), aggregateId.getId(), prepared.amount());
}
Confirm onEvent(AmountEntered amountEntered) {
return new Confirm(amountEntered.sourceId(), amountEntered.amount());
}
UnlockAmount onEvent(EntryFailed entryFailed) {
return new UnlockAmount(entryFailed.sourceId(), entryFailed.amount());
}
}
单元测试
internal class AccountKTest {
@Test
fun createAccount() {
aggregateVerifier<Account, AccountState>()
.given()
.`when`(CreateAccount("name", 100))
.expectEventType(AccountCreated::class.java)
.expectState {
assertThat(it.name, equalTo("name"))
assertThat(it.balanceAmount, equalTo(100))
}
.verify()
}
}
领域驱动设计之银行转账:Wow框架实战的更多相关文章
- 基于“事件”驱动的领域驱动设计(DDD)框架分析
摘抄自 从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven ...
- [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店
一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...
- 【无私分享:ASP.NET CORE 项目实战(第三章)】EntityFramework下领域驱动设计的应用
目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 在我们 [无私分享:从入门到精通ASP.NET MVC] 系列中,我们其实也是有DDD思想的,但是没有完全的去实现,因为并不是 ...
- [.NET领域驱动设计实战系列]专题十一:.NET 领域驱动设计实战系列总结
一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核心复杂性应对之道.实现领域驱动设计 ...
- [.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst
一.前言 从去年已经接触领域驱动设计(Domain-Driven Design)了,当时就想自己搭建一个DDD框架,所以当时看了很多DDD方面的书,例如领域驱动模式与实战,领域驱动设计:软件核心复杂性 ...
- NET 领域驱动设计实战系列总结
NET 领域驱动设计实战系列总结 一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核 ...
- [转] DDD领域驱动设计框架分享
从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven desi ...
- 领域驱动设计业务框架DMVP
DMVP,全称DDD-MVP,是基于领域驱动设计(DDD)搭建的业务框架,整体设计符合DDD领域模型的规范,业务上达成了领域模型和代码的一一映射,技术上达成了高内聚低耦合的架构设计,开发人员不需要关注 ...
- 基于事件驱动的DDD领域驱动设计框架分享(附源代码)
原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...
- .NET领域驱动设计—初尝(一:疑问、模式、原则、工具、过程、框架、实践)
.NET领域驱动设计—初尝(一:疑问.模式.原则.工具.过程.框架.实践) 2013-04-07 17:35:27 标签:.NET DDD 驱动设计 原创作品,允许转载,转载时请务必以超链接形式标明 ...
随机推荐
- 使用 Go 语言实现二叉搜索树
原文链接: 使用 Go 语言实现二叉搜索树 二叉树是一种常见并且非常重要的数据结构,在很多项目中都能看到二叉树的身影. 它有很多变种,比如红黑树,常被用作 std::map 和 std::set 的底 ...
- python-多继承构造函数声明问题
背景 有场景分别定义两组逻辑,随后有统一入口做基类属性的整合 其中两组逻辑的积累构造函数定义入参不同 设计类继承图如: 实际的使用方式抽象为[使用] 小节 实际开发过程中遇到问题 先说结论 pytho ...
- 【Unity3D】调整屏幕亮度、饱和度、对比度
1 屏幕后处理流程 调整屏幕亮度.饱和度.对比度,需要使用到屏幕后处理技术.因此,本文将先介绍屏幕后处理流程,再介绍调整屏幕亮度.饱和度.对比度的实现. 本文完整资源见→Unity3D调整屏幕 ...
- 使用canvas(2d)+js实现一个简单的傅里叶级数绘制方波图
先看效果 查看页面右下角,嘿嘿 简要说明 创建具有不同半径与角速度的圆集合:(截图中展现的效果为5个,代码是30个,运行后效果会不同) const getCircles = (N = 10) => ...
- Docker Dockerfile指令大全
FROM-指定基础镜像 指定基础镜像,并且Dockerfile中第一条指令必须是FROM指令,且在同一个Dockerfile中创建多个镜像时,可以使用多个FROM指令. # 语法格式 FROM < ...
- ATtiny88初体验(三):串口
ATtiny88初体验(三):串口 ATtiny88单片机不包含串口模块,因此只能使用软件方式模拟串口时序. 串口通信时序通常由起始位.数据位.校验位和停止位四个部分组成,常见的配置为1位起始位.8位 ...
- ImGui界面优化:使用图标字体、隐藏主窗口标题栏
目录 使用图标字体 扩展:内存加载字体 隐藏主窗口标题栏 增加程序退出 改进HideTabBar 窗口最大化 总结 本文主要介绍ImGui应用中的一些界面优化方法,如果是第一次使用ImGui推荐从上一 ...
- P4327题解
思路 分组计算 以下图为例: ..#.. .#.. .*.. .#.. .#.#. #.#. *.*. #.#. #.X.# .X.* .X.* .X.# .#.#. #.#. *.*. #.#. . ...
- 深入理解Linux内核——内存管理(1)
提要:本系列文章主要参考MIT 6.828课程以及两本书籍<深入理解Linux内核> <深入Linux内核架构>对Linux内核内容进行总结. 内存管理的实现覆盖了多个领域: ...
- C++笔记(自用)
<Effective C++> 条款11 在operator=中处理"自我赋值" 自我赋值 证同测试: if(this==&rhs)return*this; 影 ...