范畴驱动规划之银行转账:Wow结构实战
范畴驱动规划之银行转账: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()
}
}