面向對象
一.業務開發常用的基於貧血模型的MVC架構違背OOP嗎?
1.什麼是基於貧血模型的傳統開發模式?
像 UserBo 這樣,只包含數據,不包含業務邏輯的類,就叫作貧血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基於貧血模型設計的。這種貧血模型將數據與操作分離,破壞了面向對象的封裝特性,是一種典型的面向過程的編程風格
2.什麼是基於充血模型的 DDD 開發模式?
- 充血模型(Rich Domain Model)正好相反,數據和對應的業務邏輯被封裝到同一個類中。因此,這種充血模型滿足面向對象的封裝特性,是典型的面向對象編程風格。
- 領域驅動模型:
- 領域驅動設計,即 DDD,主要是用來指導如何解耦業務系統,劃分業務模塊,定義業務領域模型及其交互
- 在基於充血模型的 DDD 開發模式中,Service 層包含 Service 類和 Domain 類兩部分。Domain 就相當於貧血模型中的 BO。不過,Domain 與 BO 的區別在於它是基於充血模型開發的,既包含數據,也包含業務邏輯。而 Service 類變得非常單薄。總結一下的話就是,基於貧血模型的傳統的開發模式,重 Service 輕 BO;基於充血模型的 DDD 開發模式,輕 Service 重 Domain。
3.什麼項目應該考慮使用基於充血模型的 DDD 開發模式?
- 基於貧血模型的傳統的開發模式,比較適合業務比較簡單的系統開發。相對應的,基於充血模型的 DDD 開發模式,更適合業務複雜的系統開發。比如,包含各種利息計算模型、還款模型等複雜業務的金融系統。
- 開發流程不一樣,我們平時的開發,大部分都是 SQL 驅動(SQL-Driven)的開發模式。我們接到一個後端接口的開發需求的時候,就去看接口需要的數據對應到數據庫中,需要哪張表或者哪幾張表,然後思考如何編寫 SQL 語句來獲取數據。之後就是定義 Entity、BO、VO,然後模板式地往對應的 Repository、Service、Controller 類中添加代碼。
二.如何利用基於充血模型的DDD開發一個虛擬錢包系統?
- 現在,我們再思考這樣一個問題:充值、提現、支付這些業務交易類型,是否應該讓虛擬錢包系統感知?換句話說,我們是否應該在虛擬錢包系統的交易流水中記錄這三種類型?
答案是否定的。虛擬錢包系統不應該感知具體的業務交易類型。我們前面講到,虛擬錢包支持的操作,僅僅是餘額的加加減減操作,不涉及複雜業務概念,職責單一、功能通用。如果耦合太多業務概念到裏面,勢必影響系統的通用性,而且還會導致系統越做越複雜。因此,我們不希望將充值、支付、提現這樣的業務概念添加到虛擬錢包系統中。
1. 基於貧血模型的傳統開發模式
- Controller 中,接口實現比較簡單,主要就是調用 Service 的方法
public class VirtualWalletController {
// 通過構造函數或者IOC框架注入
private VirtualWalletService virtualWalletService;
public BigDecimal getBalance(Long walletId) { ... } //查詢餘額
public void debit(Long walletId, BigDecimal amount) { ... } //出賬
public void credit(Long walletId, BigDecimal amount) { ... } //入賬
public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { ...} //轉賬
}
- Service 和 BO 負責核心業務邏輯,Repository 和 Entity 負責數據存取
public class VirtualWalletBo {//省略getter/setter/constructor方法
private Long id;
private Long createTime;
private BigDecimal balance;
}
public class VirtualWalletService {
// 通過構造函數或者IOC框架注入
private VirtualWalletRepository walletRepo;
private VirtualWalletTransactionRepository transactionRepo;
public VirtualWalletBo getVirtualWallet(Long walletId) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWalletBo walletBo = convert(walletEntity);
return walletBo;
}
public BigDecimal getBalance(Long walletId) {
return walletRepo.getBalance(walletId);
}
public void debit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
BigDecimal balance = walletEntity.getBalance();
if (balance.compareTo(amount) < 0) {
throw new NoSufficientBalanceException(...);
}
walletRepo.updateBalance(walletId, balance.subtract(amount));
}
public void credit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
BigDecimal balance = walletEntity.getBalance();
walletRepo.updateBalance(walletId, balance.add(amount));
}
public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setFromWalletId(fromWalletId);
transactionEntity.setToWalletId(toWalletId);
transactionEntity.setStatus(Status.TO_BE_EXECUTED);
Long transactionId = transactionRepo.saveTransaction(transactionEntity);
try {
debit(fromWalletId, amount);
credit(toWalletId, amount);
} catch (InsufficientBalanceException e) {
transactionRepo.updateStatus(transactionId, Status.CLOSED);
...rethrow exception e...
} catch (Exception e) {
transactionRepo.updateStatus(transactionId, Status.FAILED);
...rethrow exception e...
}
transactionRepo.updateStatus(transactionId, Status.EXECUTED);
}
}
2. 基於充血模型的 DDD 開發模式
- 基於充血模型的 DDD 開發模式,跟基於貧血模型的傳統開發模式的主要區別就在 Service 層,Controller 層和 Repository 層的代碼基本上相同
- 把虛擬錢包 VirtualWallet 類設計成一個充血的 Domain 領域模型,並且將原來在 Service 類中的部分業務邏輯移動到 VirtualWallet 類中,讓 Service 類的實現依賴 VirtualWallet 類
public class VirtualWallet { // Domain領域模型(充血模型)
private Long id;
private Long createTime = System.currentTimeMillis();;
private BigDecimal balance = BigDecimal.ZERO;
public VirtualWallet(Long preAllocatedId) {
this.id = preAllocatedId;
}
public BigDecimal balance() {
return this.balance;
}
public void debit(BigDecimal amount) {
if (this.balance.compareTo(amount) < 0) {
throw new InsufficientBalanceException(...);
}
this.balance.subtract(amount);
}
public void credit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException(...);
}
this.balance.add(amount);
}
}
public class VirtualWalletService {
// 通過構造函數或者IOC框架注入
private VirtualWalletRepository walletRepo;
private VirtualWalletTransactionRepository transactionRepo;
public VirtualWallet getVirtualWallet(Long walletId) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
return wallet;
}
public BigDecimal getBalance(Long walletId) {
return walletRepo.getBalance(walletId);
}
public void debit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
wallet.debit(amount);
walletRepo.updateBalance(walletId, wallet.balance());
}
public void credit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
wallet.credit(amount);
walletRepo.updateBalance(walletId, wallet.balance());
}
public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
//...跟基於貧血模型的傳統開發模式的代碼一樣...
}
}
- 領域模型 VirtualWallet 類很單薄,包含的業務邏輯很簡單。相對於原來的貧血模型的設計思路,這種充血模型的設計思路,貌似並沒有太大優勢。你說得沒錯!這也是大部分業務系統都使用基於貧血模型開發的原因。不過,如果虛擬錢包系統需要支持更復雜的業務邏輯,那充血模型的優勢就顯現出來了。比如,我們要支持透支一定額度和凍結部分餘額的功能。這個時候,我們重新來看一下 VirtualWallet 類的實現代碼。
public class VirtualWallet {
private Long id;
private Long createTime = System.currentTimeMillis();;
private BigDecimal balance = BigDecimal.ZERO;
private boolean isAllowedOverdraft = true;
private BigDecimal overdraftAmount = BigDecimal.ZERO;
private BigDecimal frozenAmount = BigDecimal.ZERO;
public VirtualWallet(Long preAllocatedId) {
this.id = preAllocatedId;
}
public void freeze(BigDecimal amount) { ... }
public void unfreeze(BigDecimal amount) { ...}
public void increaseOverdraftAmount(BigDecimal amount) { ... }
public void decreaseOverdraftAmount(BigDecimal amount) { ... }
public void closeOverdraft() { ... }
public void openOverdraft() { ... }
public BigDecimal balance() {
return this.balance;
}
public BigDecimal getAvaliableBalance() {
BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount);
if (isAllowedOverdraft) {
totalAvaliableBalance += this.overdraftAmount;
}
return totalAvaliableBalance;
}
public void debit(BigDecimal amount) {
BigDecimal totalAvaliableBalance = getAvaliableBalance();
if (totoalAvaliableBalance.compareTo(amount) < 0) {
throw new InsufficientBalanceException(...);
}
this.balance.subtract(amount);
}
public void credit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException(...);
}
this.balance.add(amount);
}
}
- Service 類主要有下面這樣幾個職責
- Service 類負責與 Repository 交流。在我的設計與代碼實現中,VirtualWalletService 類負責與 Repository 層打交道,調用 Respository 類的方法,獲取數據庫中的數據,轉化成領域模型 VirtualWallet,然後由領域模型 VirtualWallet 來完成業務邏輯,最後調用 Repository 類的方法,將數據存回數據庫。
- Service 類負責跨領域模型的業務聚合功能。VirtualWalletService 類中的 transfer() 轉賬函數會涉及兩個錢包的操作,因此這部分業務邏輯無法放到 VirtualWallet 類中,所以,我們暫且把轉賬業務放到 VirtualWalletService 類中了。當然,雖然功能演進,使得轉賬業務變得複雜起來之後,我們也可以將轉賬業務抽取出來,設計成一個獨立的領域模型
- Service 類負責一些非功能性及與三方系統交互的工作。比如冪等、事務、發郵件、發消息、記錄日誌、調用其他系統的 RPC 接口等,都可以放到 Service 類中。
3.回顧
- 基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,主要區別在 Service 層。在基於充血模型的開發模式下,我們將部分原來在 Service 類中的業務邏輯移動到了一個充血的 Domain 領域模型中,讓 Service 類的實現依賴這個 Domain 類
- 在基於充血模型的 DDD 開發模式下,Service 類並不會完全移除,而是負責一些不適合放在 Domain 類中的功能。比如,負責與 Repository 層打交道、跨領域模型的業務聚合功能、冪等事務等非功能性的工作
- 基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,Controller 層和 Repository 層的代碼基本上相同。這是因爲,Repository 層的 Entity 生命週期有限,Controller 層的 VO 只是單純作爲一種 DTO。兩部分的業務邏輯都不會太複雜。業務邏輯主要集中在 Service 層。所以,Repository 層和 Controller 層繼續沿用貧血模型的設計思路是沒有問題的。
三.如何對接口鑑權這樣一個功能開發做面向對象分析?
1. 如何進行面向對象設計?
- 劃分職責進而識別出有哪些類
那就是根據需求描述,把其中涉及的功能點,一個一個羅列出來,然後再去看哪些功能點職責相近,操作同樣的屬性,可否應該歸爲同一個類
- 例子: 拆分功能點
- 把 URL、AppID、密碼、時間戳拼接爲一個字符串;
- 對字符串通過加密算法加密生成 token;
- 將 token、AppID、時間戳拼接到 URL 中,形成新的 URL;
- 解析 URL,得到 token、AppID、時間戳等信息;
- 從存儲中取出 AppID 和對應的密碼;
- 根據時間戳判斷 token 是否過期失效;
- 驗證兩個 token 是否匹配;
- 拆分結果:
1、2、6、7 都是跟 token 有關,負責 token 的生成、驗證;3、4 都是在處理 URL,負責 URL 的拼接、解析;5 是操作 AppID 和密碼,負責從存儲中讀取 AppID 和密碼。所以,我們可以粗略地得到三個核心的類:AuthToken、Url、CredentialStorage。AuthToken 負責實現 1、2、6、7 這四個操作;Url 負責 3、4 兩個操作;CredentialStorage 負責 5 這個操作。