银行账户转账案例

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

银行转账流程

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

运行案例

自动生成 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()
}
}

领域驱动设计之银行转账:Wow框架实战的更多相关文章

  1. 基于“事件”驱动的领域驱动设计(DDD)框架分析

    摘抄自 从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven ...

  2. [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店

    一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...

  3. 【无私分享:ASP.NET CORE 项目实战(第三章)】EntityFramework下领域驱动设计的应用

    目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 在我们 [无私分享:从入门到精通ASP.NET MVC] 系列中,我们其实也是有DDD思想的,但是没有完全的去实现,因为并不是 ...

  4. [.NET领域驱动设计实战系列]专题十一:.NET 领域驱动设计实战系列总结

    一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核心复杂性应对之道.实现领域驱动设计 ...

  5. [.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst

    一.前言 从去年已经接触领域驱动设计(Domain-Driven Design)了,当时就想自己搭建一个DDD框架,所以当时看了很多DDD方面的书,例如领域驱动模式与实战,领域驱动设计:软件核心复杂性 ...

  6. NET 领域驱动设计实战系列总结

    NET 领域驱动设计实战系列总结 一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核 ...

  7. [转] DDD领域驱动设计框架分享

    从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven desi ...

  8. 领域驱动设计业务框架DMVP

    DMVP,全称DDD-MVP,是基于领域驱动设计(DDD)搭建的业务框架,整体设计符合DDD领域模型的规范,业务上达成了领域模型和代码的一一映射,技术上达成了高内聚低耦合的架构设计,开发人员不需要关注 ...

  9. 基于事件驱动的DDD领域驱动设计框架分享(附源代码)

    原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...

  10. .NET领域驱动设计—初尝(一:疑问、模式、原则、工具、过程、框架、实践)

     .NET领域驱动设计—初尝(一:疑问.模式.原则.工具.过程.框架.实践) 2013-04-07 17:35:27 标签:.NET DDD 驱动设计 原创作品,允许转载,转载时请务必以超链接形式标明 ...

随机推荐

  1. 使用 Go 语言实现二叉搜索树

    原文链接: 使用 Go 语言实现二叉搜索树 二叉树是一种常见并且非常重要的数据结构,在很多项目中都能看到二叉树的身影. 它有很多变种,比如红黑树,常被用作 std::map 和 std::set 的底 ...

  2. python-多继承构造函数声明问题

    背景 有场景分别定义两组逻辑,随后有统一入口做基类属性的整合 其中两组逻辑的积累构造函数定义入参不同 设计类继承图如: 实际的使用方式抽象为[使用] 小节 实际开发过程中遇到问题 先说结论 pytho ...

  3. 【Unity3D】调整屏幕亮度、饱和度、对比度

    1 屏幕后处理流程 ​ 调整屏幕亮度.饱和度.对比度,需要使用到屏幕后处理技术.因此,本文将先介绍屏幕后处理流程,再介绍调整屏幕亮度.饱和度.对比度的实现. ​ 本文完整资源见→Unity3D调整屏幕 ...

  4. 使用canvas(2d)+js实现一个简单的傅里叶级数绘制方波图

    先看效果 查看页面右下角,嘿嘿 简要说明 创建具有不同半径与角速度的圆集合:(截图中展现的效果为5个,代码是30个,运行后效果会不同) const getCircles = (N = 10) => ...

  5. Docker Dockerfile指令大全

    FROM-指定基础镜像 指定基础镜像,并且Dockerfile中第一条指令必须是FROM指令,且在同一个Dockerfile中创建多个镜像时,可以使用多个FROM指令. # 语法格式 FROM < ...

  6. ATtiny88初体验(三):串口

    ATtiny88初体验(三):串口 ATtiny88单片机不包含串口模块,因此只能使用软件方式模拟串口时序. 串口通信时序通常由起始位.数据位.校验位和停止位四个部分组成,常见的配置为1位起始位.8位 ...

  7. ImGui界面优化:使用图标字体、隐藏主窗口标题栏

    目录 使用图标字体 扩展:内存加载字体 隐藏主窗口标题栏 增加程序退出 改进HideTabBar 窗口最大化 总结 本文主要介绍ImGui应用中的一些界面优化方法,如果是第一次使用ImGui推荐从上一 ...

  8. P4327题解

    思路 分组计算 以下图为例: ..#.. .#.. .*.. .#.. .#.#. #.#. *.*. #.#. #.X.# .X.* .X.* .X.# .#.#. #.#. *.*. #.#. . ...

  9. 深入理解Linux内核——内存管理(1)

    提要:本系列文章主要参考MIT 6.828课程以及两本书籍<深入理解Linux内核> <深入Linux内核架构>对Linux内核内容进行总结. 内存管理的实现覆盖了多个领域: ...

  10. C++笔记(自用)

    <Effective C++> 条款11 在operator=中处理"自我赋值" 自我赋值 证同测试: if(this==&rhs)return*this; 影 ...