領域驅動設計之銀行轉賬:Wow框架實戰

2023-11-21 18:00:56

銀行賬戶轉賬案例

銀行賬戶轉賬案例是一個經典的領域驅動設計(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)建模

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()
    }
}