领域驱动设计之银行转账: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() } }

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
MySQL 的锁等待超时到底是怎么回事?
锁等待之后有两种结果:获得锁、超时,这一期先来看看锁等待超时之后都要干什么? 作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。 正文 超时检查线程 InnoDB 有个名为 ib_srv_lock_to 的后台线程,每秒进行一次超时检查,看看是否有锁等待超时的事务。 前面介绍锁等待时,我们介绍过:如果事务加锁进入锁等待状态,会给后台线程发送通知,告诉后台线程发生了锁等待,这个后台线程就是超时检查线程。 既然每个事务进入锁等待状态都会通知超时检查线程,那么,每秒进行一次超时检查的说法是不是有问题? 这自然是没问题的了。 因为超时检查线程是个多面手,它不只会进行超时检查,还会做别的事情,那就是死锁检查。 事务进入锁等待状态时,给超时检查线程发送通知,是为了触发这个线程马上进行死锁检查。 收到通知之后,超时检查线程会判断距离上一次超时检查的时间。如果小于 1s,就不进行超时检查,大于等于 ...
- 下一篇
将传统应用带入浏览器的开源先锋「GitHub 热点速览」
现代浏览器已经不再是简单的浏览网页的工具,其潜能正在通过技术不断地被挖掘和扩展。得益于 WebAssembly 等技术的出现,让浏览器能够以接近原生的速度执行非 JavaScript 语言编写的程序,从而打开了浏览器的"潘多拉魔盒"。 开源组织 Leaning Technologies 正是这一方面的先锋,他们开发的 Cheerp、CheerpJ 和 CheerpX 等开源项目,使 C/C++、Java、Flash 和 x86 程序能够在浏览器中流畅地运行,它们正在逐步打破传统桌面应用程序和 Web 应用之间的"壁垒"。 Cheerp:运行在浏览器里的 C/C++ 编译器 CheerpJ:运行在浏览器里的 Java 虚拟机和运行时环境 CheerpX:运行在浏览器里的 x86 虚拟机 比如本周的开源热搜项目,基于 CheerpX 引擎的 WebVM 开源项目,它支持用户在浏览器中运行完整的 Linux 环境,无需下载和安装。开源的 Web 应用防火墙 BunkerWeb,让你的 Web 默认配置变得安全。极小的 fetch 封装库 Wretch,让前端请求数据更加轻松惬意。在浏览器里控...
相关文章
文章评论
共有0条评论来说两句吧...