005.設計原則與思想:面向對象

一.業務開發常用的基於貧血模型的MVC架構違背OOP嗎?

1.什麼是基於貧血模型的傳統開發模式?

像 UserBo 這樣,只包含數據,不包含業務邏輯的類,就叫作貧血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基於貧血模型設計的。這種貧血模型將數據與操作分離,破壞了面向對象的封裝特性,是一種典型的面向過程的編程風格

2.什麼是基於充血模型的 DDD 開發模式?
  1. 充血模型(Rich Domain Model)正好相反,數據和對應的業務邏輯被封裝到同一個類中。因此,這種充血模型滿足面向對象的封裝特性,是典型的面向對象編程風格。
  2. 領域驅動模型:
  1. 領域驅動設計,即 DDD,主要是用來指導如何解耦業務系統,劃分業務模塊,定義業務領域模型及其交互
  2. 在基於充血模型的 DDD 開發模式中,Service 層包含 Service 類和 Domain 類兩部分。Domain 就相當於貧血模型中的 BO。不過,Domain 與 BO 的區別在於它是基於充血模型開發的,既包含數據,也包含業務邏輯。而 Service 類變得非常單薄。總結一下的話就是,基於貧血模型的傳統的開發模式,重 Service 輕 BO;基於充血模型的 DDD 開發模式,輕 Service 重 Domain。
3.什麼項目應該考慮使用基於充血模型的 DDD 開發模式?
  1. 基於貧血模型的傳統的開發模式,比較適合業務比較簡單的系統開發。相對應的,基於充血模型的 DDD 開發模式,更適合業務複雜的系統開發。比如,包含各種利息計算模型、還款模型等複雜業務的金融系統。
  2. 開發流程不一樣,我們平時的開發,大部分都是 SQL 驅動(SQL-Driven)的開發模式。我們接到一個後端接口的開發需求的時候,就去看接口需要的數據對應到數據庫中,需要哪張表或者哪幾張表,然後思考如何編寫 SQL 語句來獲取數據。之後就是定義 Entity、BO、VO,然後模板式地往對應的 Repository、Service、Controller 類中添加代碼。

二.如何利用基於充血模型的DDD開發一個虛擬錢包系統?

  1. 現在,我們再思考這樣一個問題:充值、提現、支付這些業務交易類型,是否應該讓虛擬錢包系統感知?換句話說,我們是否應該在虛擬錢包系統的交易流水中記錄這三種類型?

答案是否定的。虛擬錢包系統不應該感知具體的業務交易類型。我們前面講到,虛擬錢包支持的操作,僅僅是餘額的加加減減操作,不涉及複雜業務概念,職責單一、功能通用。如果耦合太多業務概念到裏面,勢必影響系統的通用性,而且還會導致系統越做越複雜。因此,我們不希望將充值、支付、提現這樣的業務概念添加到虛擬錢包系統中。

1. 基於貧血模型的傳統開發模式
  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) { ...} //轉賬
}
  1. 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 開發模式
  1. 基於充血模型的 DDD 開發模式,跟基於貧血模型的傳統開發模式的主要區別就在 Service 層,Controller 層和 Repository 層的代碼基本上相同
  2. 把虛擬錢包 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) {
    //...跟基於貧血模型的傳統開發模式的代碼一樣...
  }
}
  1. 領域模型 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);
  }
}
  1. Service 類主要有下面這樣幾個職責
  1. Service 類負責與 Repository 交流。在我的設計與代碼實現中,VirtualWalletService 類負責與 Repository 層打交道,調用 Respository 類的方法,獲取數據庫中的數據,轉化成領域模型 VirtualWallet,然後由領域模型 VirtualWallet 來完成業務邏輯,最後調用 Repository 類的方法,將數據存回數據庫。
  2. Service 類負責跨領域模型的業務聚合功能。VirtualWalletService 類中的 transfer() 轉賬函數會涉及兩個錢包的操作,因此這部分業務邏輯無法放到 VirtualWallet 類中,所以,我們暫且把轉賬業務放到 VirtualWalletService 類中了。當然,雖然功能演進,使得轉賬業務變得複雜起來之後,我們也可以將轉賬業務抽取出來,設計成一個獨立的領域模型
  3. Service 類負責一些非功能性及與三方系統交互的工作。比如冪等、事務、發郵件、發消息、記錄日誌、調用其他系統的 RPC 接口等,都可以放到 Service 類中。
3.回顧
  1. 基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,主要區別在 Service 層。在基於充血模型的開發模式下,我們將部分原來在 Service 類中的業務邏輯移動到了一個充血的 Domain 領域模型中,讓 Service 類的實現依賴這個 Domain 類
  2. 在基於充血模型的 DDD 開發模式下,Service 類並不會完全移除,而是負責一些不適合放在 Domain 類中的功能。比如,負責與 Repository 層打交道、跨領域模型的業務聚合功能、冪等事務等非功能性的工作
  3. 基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,Controller 層和 Repository 層的代碼基本上相同。這是因爲,Repository 層的 Entity 生命週期有限,Controller 層的 VO 只是單純作爲一種 DTO。兩部分的業務邏輯都不會太複雜。業務邏輯主要集中在 Service 層。所以,Repository 層和 Controller 層繼續沿用貧血模型的設計思路是沒有問題的。

三.如何對接口鑑權這樣一個功能開發做面向對象分析?

1. 如何進行面向對象設計?
  1. 劃分職責進而識別出有哪些類

那就是根據需求描述,把其中涉及的功能點,一個一個羅列出來,然後再去看哪些功能點職責相近,操作同樣的屬性,可否應該歸爲同一個類

  1. 例子: 拆分功能點
  1. 把 URL、AppID、密碼、時間戳拼接爲一個字符串;
  2. 對字符串通過加密算法加密生成 token;
  3. 將 token、AppID、時間戳拼接到 URL 中,形成新的 URL;
  4. 解析 URL,得到 token、AppID、時間戳等信息;
  5. 從存儲中取出 AppID 和對應的密碼;
  6. 根據時間戳判斷 token 是否過期失效;
  7. 驗證兩個 token 是否匹配;
  1. 拆分結果:

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 這個操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章