领域驱动设计之银行转账: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());
}
}
单元测试
借助 wow 单元测试套件,可以轻松的编写聚合根和 saga 的单元测试。从而提升代码覆盖率,保证代码质量。
使用
aggregateverifier进行聚合根单元测试,可以有效的减少单元测试的编写工作量。
account聚合根单元测试
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()
}
@test
fun prepare() {
aggregateverifier<account, accountstate>()
.given(accountcreated("name", 100))
.`when`(prepare("name", 100))
.expecteventtype(amountlocked::class.java, prepared::class.java)
.expectstate {
assertthat(it.name, equalto("name"))
assertthat(it.balanceamount, equalto(0))
}
.verify()
}
@test
fun entry() {
val aggregateid = globalidgenerator.generateasstring()
aggregateverifier<account, accountstate>(aggregateid)
.given(accountcreated("name", 100))
.`when`(entry(aggregateid, "sourceid", 100))
.expecteventtype(amountentered::class.java)
.expectstate {
assertthat(it.name, equalto("name"))
assertthat(it.balanceamount, equalto(200))
}
.verify()
}
@test
fun entrygivenfrozen() {
val aggregateid = globalidgenerator.generateasstring()
aggregateverifier<account, accountstate>(aggregateid)
.given(accountcreated("name", 100), accountfrozen(""))
.`when`(entry(aggregateid, "sourceid", 100))
.expecteventtype(entryfailed::class.java)
.expectstate {
assertthat(it.name, equalto("name"))
assertthat(it.balanceamount, equalto(100))
assertthat(it.isfrozen, equalto(true))
}
.verify()
}
@test
fun confirm() {
val aggregateid = globalidgenerator.generateasstring()
aggregateverifier<account, accountstate>(aggregateid)
.given(accountcreated("name", 100), amountlocked(100))
.`when`(confirm(aggregateid, 100))
.expecteventtype(confirmed::class.java)
.expectstate {
assertthat(it.name, equalto("name"))
assertthat(it.balanceamount, equalto(0))
assertthat(it.lockedamount, equalto(0))
assertthat(it.isfrozen, equalto(false))
}
.verify()
}
@test
fun unlockamount() {
val aggregateid = globalidgenerator.generateasstring()
aggregateverifier<account, accountstate>(aggregateid)
.given(accountcreated("name", 100), amountlocked(100))
.`when`(unlockamount(aggregateid, 100))
.expecteventtype(amountunlocked::class.java)
.expectstate {
assertthat(it.name, equalto("name"))
assertthat(it.balanceamount, equalto(100))
assertthat(it.lockedamount, equalto(0))
assertthat(it.isfrozen, equalto(false))
}
.verify()
}
@test
fun freezeaccount() {
val aggregateid = globalidgenerator.generateasstring()
aggregateverifier<account, accountstate>(aggregateid)
.given(accountcreated("name", 100))
.`when`(freezeaccount(""))
.expecteventtype(accountfrozen::class.java)
.expectstate {
assertthat(it.name, equalto("name"))
assertthat(it.balanceamount, equalto(100))
assertthat(it.lockedamount, equalto(0))
assertthat(it.isfrozen, equalto(true))
}
.verify()
}
}
使用
sagaverifier进行 saga 单元测试,可以有效的减少单元测试的编写工作量。
transfersaga单元测试
internal class transfersagatest {
@test
fun onprepared() {
val event = prepared("to", 1)
sagaverifier<transfersaga>()
.`when`(event)
.expectcommandbody<entry> {
assertthat(it.id, equalto(event.to))
assertthat(it.amount, equalto(event.amount))
}
.verify()
}
@test
fun onamountentered() {
val event = amountentered("sourceid", 1)
sagaverifier<transfersaga>()
.`when`(event)
.expectcommandbody<confirm> {
assertthat(it.id, equalto(event.sourceid))
assertthat(it.amount, equalto(event.amount))
}
.verify()
}
@test
fun onentryfailed() {
val event = entryfailed("sourceid", 1)
sagaverifier<transfersaga>()
.`when`(event)
.expectcommandbody<unlockamount> {
assertthat(it.id, equalto(event.sourceid))
assertthat(it.amount, equalto(event.amount))
}
.verify()
}
}
发表评论