領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)之銀行轉(zhuǎn)賬:Wow框架實(shí)戰(zhàn)
銀行賬戶轉(zhuǎn)賬案例是一個(gè)經(jīng)典的領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)應(yīng)用場(chǎng)景。接下來(lái)我們通過(guò)一個(gè)簡(jiǎn)單的銀行賬戶轉(zhuǎn)賬案例,來(lái)了解如何使用 Wow 進(jìn)行領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)以及服務(wù)開發(fā)。
銀行轉(zhuǎn)賬流程
- 準(zhǔn)備轉(zhuǎn)賬(Prepare): 用戶發(fā)起轉(zhuǎn)賬請(qǐng)求,觸發(fā) Prepare 步驟。這個(gè)步驟會(huì)向源賬戶發(fā)送準(zhǔn)備轉(zhuǎn)賬的請(qǐng)求。
- 校驗(yàn)余額(CheckBalance): 源賬戶在收到準(zhǔn)備轉(zhuǎn)賬請(qǐng)求后,會(huì)執(zhí)行校驗(yàn)余額的操作,確保賬戶有足夠的余額進(jìn)行轉(zhuǎn)賬。
- 鎖定金額(LockAmount): 如果余額足夠,源賬戶會(huì)鎖定轉(zhuǎn)賬金額,防止其他操作干擾。
- 入賬(Entry): 接著,轉(zhuǎn)賬流程進(jìn)入到目標(biāo)賬戶,執(zhí)行入賬操作。
- 確認(rèn)轉(zhuǎn)賬(Confirm): 如果入賬成功,確認(rèn)轉(zhuǎn)賬;否則,執(zhí)行解鎖金額操作。
- 成功路徑(Success): 如果一切順利,完成轉(zhuǎn)賬流程。
- 失敗路徑(Fail): 如果入賬失敗,執(zhí)行解鎖金額操作,并處理失敗情況。
運(yùn)行案例
- 運(yùn)行 TransferExampleServer.java
- 查看 Swagger-UI : http://localhost:8080/swagger-ui.html
- 執(zhí)行 API 測(cè)試:Transfer.http
自動(dòng)生成 API 端點(diǎn)
運(yùn)行之后,訪問(wèn) Swagger-UI : http://localhost:8080/swagger-ui.html 。
該 RESTful API 端點(diǎn)是由 Wow 自動(dòng)生成的,無(wú)需手動(dòng)編寫。
模塊劃分
模塊 | 說(shuō)明 |
---|---|
example-transfer-api | API 層,定義聚合命令(Command)、領(lǐng)域事件(Domain Event)以及查詢視圖模型(Query View Model),這個(gè)模塊充當(dāng)了各個(gè)模塊之間通信的“發(fā)布語(yǔ)言”。 |
example-transfer-domain | 領(lǐng)域?qū)樱酆细蜆I(yè)務(wù)約束的實(shí)現(xiàn)。聚合根:領(lǐng)域模型的入口點(diǎn),負(fù)責(zé)協(xié)調(diào)領(lǐng)域?qū)ο蟮牟僮鳌I(yè)務(wù)約束:包括驗(yàn)證規(guī)則、領(lǐng)域事件的處理等。 |
example-transfer-server | 宿主服務(wù),應(yīng)用程序的啟動(dòng)點(diǎn)。負(fù)責(zé)整合其他模塊,并提供應(yīng)用程序的入口。涉及配置依賴項(xiàng)、連接數(shù)據(jù)庫(kù)、啟動(dòng) API 服務(wù) |
領(lǐng)域建模
狀態(tài)聚合根(AccountState
)與命令聚合根(Account
)分離設(shè)計(jì)保證了在執(zhí)行命令過(guò)程中,不會(huì)修改狀態(tài)聚合根的狀態(tài)。
狀態(tài)聚合根(AccountState)建模
public class AccountState implements Identifier {
private final String id;
private String name;
/**
* 余額
*/
private long balanceAmount = 0L;
/**
* 已鎖定金額
*/
private long lockedAmount = 0L;
/**
* 賬號(hào)已凍結(jié)標(biāo)記
*/
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("賬號(hào)已凍結(jié)無(wú)法轉(zhuǎn)賬.");
}
if (state.getBalanceAmount() < amount) {
throw new IllegalStateException("賬號(hào)余額不足.");
}
}
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());
}
}
轉(zhuǎn)賬流程管理器(TransferSaga)
轉(zhuǎn)賬流程管理器(TransferSaga
)負(fù)責(zé)協(xié)調(diào)處理轉(zhuǎn)賬的事件,并生成相應(yīng)的命令。
-
onEvent(Prepared)
: 訂閱轉(zhuǎn)賬已準(zhǔn)備就緒事件(Prepared
),并生成入賬命令(Entry
)。 -
onEvent(AmountEntered)
: 訂閱轉(zhuǎn)賬已入賬事件(AmountEntered
),并生成確認(rèn)轉(zhuǎn)賬命令(Confirm
)。 -
onEvent(EntryFailed)
: 訂閱轉(zhuǎn)賬入賬失敗事件(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());
}
}
單元測(cè)試
借助 Wow 單元測(cè)試套件,可以輕松的編寫聚合根和 Saga 的單元測(cè)試。從而提升代碼覆蓋率,保證代碼質(zhì)量。
使用
aggregateVerifier
進(jìn)行聚合根單元測(cè)試,可以有效的減少單元測(cè)試的編寫工作量。
Account
聚合根單元測(cè)試
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
進(jìn)行 Saga 單元測(cè)試,可以有效的減少單元測(cè)試的編寫工作量。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-746672.html
TransferSaga
單元測(cè)試文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-746672.html
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()
}
}
到了這里,關(guān)于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)之銀行轉(zhuǎn)賬:Wow框架實(shí)戰(zhàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!