115人参与 • 2024-05-16 • 领域驱动设计
银行账户转账案例是一个经典的领域驱动设计(ddd)应用场景。接下来我们通过一个简单的银行账户转账案例,来了解如何使用 wow 进行领域驱动设计以及服务开发。
运行之后,访问 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() } }
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论