找回密码
 立即注册
首页 业界区 业界 领域驱动设计之银行转账:Wow框架实战

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

讣丢 昨天 08:25
领域驱动设计之银行转账:Wow框架实战

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


  • 准备转账(Prepare): 用户发起转账请求,触发 Prepare 步骤。这个步骤会向源账户发送准备转账的请求。
  • 校验余额(CheckBalance): 源账户在收到准备转账请求后,会执行校验余额的操作,确保账户有足够的余额进行转账。
  • 锁定金额(LockAmount): 如果余额足够,源账户会锁定转账金额,防止其他操作干扰。
  • 入账(Entry): 接着,转账流程进入到目标账户,执行入账操作。
  • 确认转账(Confirm): 如果入账成功,确认转账;否则,执行解锁金额操作。

    • 成功路径(Success): 如果一切顺利,完成转账流程。
    • 失败路径(Fail): 如果入账失败,执行解锁金额操作,并处理失败情况。

  
1.jpg

运行案例


  • 运行 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 自动生成的,无需手动编写。
  
2.png

模块划分

模块说明example-transfer-apiAPI 层,定义聚合命令(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 的单元测试。从而提升代码覆盖率,保证代码质量。
  
3.png

使用 aggregateVerifier 进行聚合根单元测试,可以有效的减少单元测试的编写工作量。
Account 聚合根单元测试
  1. internal class AccountKTest {
  2.     @Test
  3.     fun createAccount() {
  4.         aggregateVerifier()
  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()
  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(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(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(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(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(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. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册