经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
领域驱动设计之银行转账:Wow框架实战 - Ahoo-Wang
来源:cnblogs  作者:Ahoo-Wang  时间:2023/11/22 16:46:40  对本文有异议

领域驱动设计之银行转账:Wow框架实战

银行账户转账案例是一个经典的领域驱动设计(DDD)应用场景。接下来我们通过一个简单的银行账户转账案例,来了解如何使用 Wow 进行领域驱动设计以及服务开发。

银行转账流程

  1. 准备转账(Prepare): 用户发起转账请求,触发 Prepare 步骤。这个步骤会向源账户发送准备转账的请求。
  2. 校验余额(CheckBalance): 源账户在收到准备转账请求后,会执行校验余额的操作,确保账户有足够的余额进行转账。
  3. 锁定金额(LockAmount): 如果余额足够,源账户会锁定转账金额,防止其他操作干扰。
  4. 入账(Entry): 接着,转账流程进入到目标账户,执行入账操作。
  5. 确认转账(Confirm): 如果入账成功,确认转账;否则,执行解锁金额操作。
    1. 成功路径(Success): 如果一切顺利,完成转账流程。
    2. 失败路径(Fail): 如果入账失败,执行解锁金额操作,并处理失败情况。

Saga-Transfer

运行案例

自动生成 API 端点

运行之后,访问 Swagger-UI : http://localhost:8080/swagger-ui.html
该 RESTful API 端点是由 Wow 自动生成的,无需手动编写。

Wow-Transfer

模块划分

模块 说明
example-transfer-api API 层,定义聚合命令(Command)、领域事件(Domain Event)以及查询视图模型(Query View Model),这个模块充当了各个模块之间通信的“发布语言”。
example-transfer-domain 领域层,包含聚合根和业务约束的实现。聚合根:领域模型的入口点,负责协调领域对象的操作。业务约束:包括验证规则、领域事件的处理等。
example-transfer-server 宿主服务,应用程序的启动点。负责整合其他模块,并提供应用程序的入口。涉及配置依赖项、连接数据库、启动 API 服务

领域建模

状态聚合根(AccountState)与命令聚合根(Account)分离设计保证了在执行命令过程中,不会修改状态聚合根的状态。

状态聚合根(AccountState)建模

  1. public class AccountState implements Identifier {
  2. private final String id;
  3. private String name;
  4. /**
  5. * 余额
  6. */
  7. private long balanceAmount = 0L;
  8. /**
  9. * 已锁定金额
  10. */
  11. private long lockedAmount = 0L;
  12. /**
  13. * 账号已冻结标记
  14. */
  15. private boolean frozen = false;
  16. @JsonCreator
  17. public AccountState(@JsonProperty("id") String id) {
  18. this.id = id;
  19. }
  20. @NotNull
  21. @Override
  22. public String getId() {
  23. return id;
  24. }
  25. public String getName() {
  26. return name;
  27. }
  28. public long getBalanceAmount() {
  29. return balanceAmount;
  30. }
  31. public long getLockedAmount() {
  32. return lockedAmount;
  33. }
  34. public boolean isFrozen() {
  35. return frozen;
  36. }
  37. void onSourcing(AccountCreated accountCreated) {
  38. this.name = accountCreated.name();
  39. this.balanceAmount = accountCreated.balance();
  40. }
  41. void onSourcing(AmountLocked amountLocked) {
  42. balanceAmount = balanceAmount - amountLocked.amount();
  43. lockedAmount = lockedAmount + amountLocked.amount();
  44. }
  45. void onSourcing(AmountEntered amountEntered) {
  46. balanceAmount = balanceAmount + amountEntered.amount();
  47. }
  48. void onSourcing(Confirmed confirmed) {
  49. lockedAmount = lockedAmount - confirmed.amount();
  50. }
  51. void onSourcing(AmountUnlocked amountUnlocked) {
  52. lockedAmount = lockedAmount - amountUnlocked.amount();
  53. balanceAmount = balanceAmount + amountUnlocked.amount();
  54. }
  55. void onSourcing(AccountFrozen accountFrozen) {
  56. this.frozen = true;
  57. }
  58. }

命令聚合根(Account)建模

  1. @StaticTenantId
  2. @AggregateRoot
  3. public class Account {
  4. private final AccountState state;
  5. public Account(AccountState state) {
  6. this.state = state;
  7. }
  8. AccountCreated onCommand(CreateAccount createAccount) {
  9. return new AccountCreated(createAccount.name(), createAccount.balance());
  10. }
  11. @OnCommand(returns = {AmountLocked.class, Prepared.class})
  12. List<?> onCommand(Prepare prepare) {
  13. checkBalance(prepare.amount());
  14. return List.of(new AmountLocked(prepare.amount()), new Prepared(prepare.to(), prepare.amount()));
  15. }
  16. private void checkBalance(long amount) {
  17. if (state.isFrozen()) {
  18. throw new IllegalStateException("账号已冻结无法转账.");
  19. }
  20. if (state.getBalanceAmount() < amount) {
  21. throw new IllegalStateException("账号余额不足.");
  22. }
  23. }
  24. Object onCommand(Entry entry) {
  25. if (state.isFrozen()) {
  26. return new EntryFailed(entry.sourceId(), entry.amount());
  27. }
  28. return new AmountEntered(entry.sourceId(), entry.amount());
  29. }
  30. Confirmed onCommand(Confirm confirm) {
  31. return new Confirmed(confirm.amount());
  32. }
  33. AmountUnlocked onCommand(UnlockAmount unlockAmount) {
  34. return new AmountUnlocked(unlockAmount.amount());
  35. }
  36. AccountFrozen onCommand(FreezeAccount freezeAccount) {
  37. return new AccountFrozen(freezeAccount.reason());
  38. }
  39. }

转账流程管理器(TransferSaga

转账流程管理器(TransferSaga)负责协调处理转账的事件,并生成相应的命令。

  • onEvent(Prepared): 订阅转账已准备就绪事件(Prepared),并生成入账命令(Entry)。
  • onEvent(AmountEntered): 订阅转账已入账事件(AmountEntered),并生成确认转账命令(Confirm)。
  • onEvent(EntryFailed): 订阅转账入账失败事件(EntryFailed),并生成解锁金额命令(UnlockAmount)。
  1. @StatelessSaga
  2. public class TransferSaga {
  3. Entry onEvent(Prepared prepared, AggregateId aggregateId) {
  4. return new Entry(prepared.to(), aggregateId.getId(), prepared.amount());
  5. }
  6. Confirm onEvent(AmountEntered amountEntered) {
  7. return new Confirm(amountEntered.sourceId(), amountEntered.amount());
  8. }
  9. UnlockAmount onEvent(EntryFailed entryFailed) {
  10. return new UnlockAmount(entryFailed.sourceId(), entryFailed.amount());
  11. }
  12. }

单元测试

借助 Wow 单元测试套件,可以轻松的编写聚合根和 Saga 的单元测试。从而提升代码覆盖率,保证代码质量。

example-transfer-jacoco

使用 aggregateVerifier 进行聚合根单元测试,可以有效的减少单元测试的编写工作量。

Account 聚合根单元测试

  1. internal class AccountKTest {
  2. @Test
  3. fun createAccount() {
  4. aggregateVerifier<Account, AccountState>()
  5. .given()
  6. .`when`(CreateAccount("name", 100))
  7. .expectEventType(AccountCreated::class.java)
  8. .expectState {
  9. assertThat(it.name, equalTo("name"))
  10. assertThat(it.balanceAmount, equalTo(100))
  11. }
  12. .verify()
  13. }
  14. @Test
  15. fun prepare() {
  16. aggregateVerifier<Account, AccountState>()
  17. .given(AccountCreated("name", 100))
  18. .`when`(Prepare("name", 100))
  19. .expectEventType(AmountLocked::class.java, Prepared::class.java)
  20. .expectState {
  21. assertThat(it.name, equalTo("name"))
  22. assertThat(it.balanceAmount, equalTo(0))
  23. }
  24. .verify()
  25. }
  26. @Test
  27. fun entry() {
  28. val aggregateId = GlobalIdGenerator.generateAsString()
  29. aggregateVerifier<Account, AccountState>(aggregateId)
  30. .given(AccountCreated("name", 100))
  31. .`when`(Entry(aggregateId, "sourceId", 100))
  32. .expectEventType(AmountEntered::class.java)
  33. .expectState {
  34. assertThat(it.name, equalTo("name"))
  35. assertThat(it.balanceAmount, equalTo(200))
  36. }
  37. .verify()
  38. }
  39. @Test
  40. fun entryGivenFrozen() {
  41. val aggregateId = GlobalIdGenerator.generateAsString()
  42. aggregateVerifier<Account, AccountState>(aggregateId)
  43. .given(AccountCreated("name", 100), AccountFrozen(""))
  44. .`when`(Entry(aggregateId, "sourceId", 100))
  45. .expectEventType(EntryFailed::class.java)
  46. .expectState {
  47. assertThat(it.name, equalTo("name"))
  48. assertThat(it.balanceAmount, equalTo(100))
  49. assertThat(it.isFrozen, equalTo(true))
  50. }
  51. .verify()
  52. }
  53. @Test
  54. fun confirm() {
  55. val aggregateId = GlobalIdGenerator.generateAsString()
  56. aggregateVerifier<Account, AccountState>(aggregateId)
  57. .given(AccountCreated("name", 100), AmountLocked(100))
  58. .`when`(Confirm(aggregateId, 100))
  59. .expectEventType(Confirmed::class.java)
  60. .expectState {
  61. assertThat(it.name, equalTo("name"))
  62. assertThat(it.balanceAmount, equalTo(0))
  63. assertThat(it.lockedAmount, equalTo(0))
  64. assertThat(it.isFrozen, equalTo(false))
  65. }
  66. .verify()
  67. }
  68. @Test
  69. fun unlockAmount() {
  70. val aggregateId = GlobalIdGenerator.generateAsString()
  71. aggregateVerifier<Account, AccountState>(aggregateId)
  72. .given(AccountCreated("name", 100), AmountLocked(100))
  73. .`when`(UnlockAmount(aggregateId, 100))
  74. .expectEventType(AmountUnlocked::class.java)
  75. .expectState {
  76. assertThat(it.name, equalTo("name"))
  77. assertThat(it.balanceAmount, equalTo(100))
  78. assertThat(it.lockedAmount, equalTo(0))
  79. assertThat(it.isFrozen, equalTo(false))
  80. }
  81. .verify()
  82. }
  83. @Test
  84. fun freezeAccount() {
  85. val aggregateId = GlobalIdGenerator.generateAsString()
  86. aggregateVerifier<Account, AccountState>(aggregateId)
  87. .given(AccountCreated("name", 100))
  88. .`when`(FreezeAccount(""))
  89. .expectEventType(AccountFrozen::class.java)
  90. .expectState {
  91. assertThat(it.name, equalTo("name"))
  92. assertThat(it.balanceAmount, equalTo(100))
  93. assertThat(it.lockedAmount, equalTo(0))
  94. assertThat(it.isFrozen, equalTo(true))
  95. }
  96. .verify()
  97. }
  98. }

使用 sagaVerifier 进行 Saga 单元测试,可以有效的减少单元测试的编写工作量。

TransferSaga 单元测试

  1. internal class TransferSagaTest {
  2. @Test
  3. fun onPrepared() {
  4. val event = Prepared("to", 1)
  5. sagaVerifier<TransferSaga>()
  6. .`when`(event)
  7. .expectCommandBody<Entry> {
  8. assertThat(it.id, equalTo(event.to))
  9. assertThat(it.amount, equalTo(event.amount))
  10. }
  11. .verify()
  12. }
  13. @Test
  14. fun onAmountEntered() {
  15. val event = AmountEntered("sourceId", 1)
  16. sagaVerifier<TransferSaga>()
  17. .`when`(event)
  18. .expectCommandBody<Confirm> {
  19. assertThat(it.id, equalTo(event.sourceId))
  20. assertThat(it.amount, equalTo(event.amount))
  21. }
  22. .verify()
  23. }
  24. @Test
  25. fun onEntryFailed() {
  26. val event = EntryFailed("sourceId", 1)
  27. sagaVerifier<TransferSaga>()
  28. .`when`(event)
  29. .expectCommandBody<UnlockAmount> {
  30. assertThat(it.id, equalTo(event.sourceId))
  31. assertThat(it.amount, equalTo(event.amount))
  32. }
  33. .verify()
  34. }
  35. }

原文链接:https://www.cnblogs.com/Ahoo-Wang/p/Wow-Transfer.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号