領域設計:聚合與聚合根

本文試圖回答如下問題:

  • 什麼是聚合?
  • 什麼是聚合根?
  • 如何確定聚合和聚合根?
  • Respository與DAO的區別

設計的表現力

《程序員必讀之軟件架構》一書在「軟件架構和編碼」一章有這麼一段話:

儘管很多人以組件來談論他們的軟件系統,然而代碼通常並未反映出這種結構。這就是軟件架構和依據原則編碼之間會脫節的原因之一:牆上的架構圖說的是一回事,代碼說的卻是另一回事。

個人認爲這是架構與代碼差異的一個原因。還有一個原因就是某些約束沒有在設計中體現出來,也就是說設計的表現力不夠,而這些約束需要閱讀代碼才能夠知道,這就增加了理解和使用這個組件的難度。這個問題在基於數據建模的設計方法上比較明顯。

領域設計:Entity與VO提到的淘寶購物爲例,以數據驅動的方式來設計,我們會有如下兩張表:

CREATE TABLE `order` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 `seller_id` BIGINT(11) NOT NULL COMMENT '賣家',
 `buyer_id` BIGINT(11) NOT NULL COMMENT '買家',
 `price` BIGINT(11) NOT NULL COMMENT '訂單總價格,按分計算',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

CREATE TABLE `order_detail` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 `order_id` BIGINT(11) NOT NULL COMMENT '訂單主鍵',
 `product_name` VARCHAR(50) COMMENT '產品名稱',
 `product_desc` VARCHAR(200) COMMENT '產品描述',
 `product_price` BIGINT(11) NOT NULL COMMENT '產品價格,按分計算',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

從表關係上,我們只能知道order與order_detail是一對多的關係。我們再看下面這兩張表:

CREATE TABLE `product` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 `name` VARCHAR(50) COMMENT '產品名稱',
 `desc` VARCHAR(200) COMMENT '產品描述',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

CREATE TABLE `product_comment` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 `product_id` BIGINT(11) NOT NULL COMMENT '產品',
 `cont` VARCHAR(2000) COMMENT '評價內容',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

從表關係上,我們也只能知道product與product_comment之間是一對多的關係。

那麼,請問:order與order_detail之間的關係與product與product_comment之間的關係是一樣的嗎?至少從上面的表設計上,完全看不出來!

我們需要深入到代碼,才能夠發現差異:

@Service
@Transactional
public class OrderService {
 public void createOrder(Order order,List<OrderDetail> orderDetailList) throws Exception {
 // 保存訂單
 // 保存訂單詳情
 }
 }
}

@Service
@Transactional
public class ProductService {
 public void createProduct(Product prod) throws Exception {
 // 保存產品
 }
 }
}
  • 訂單和訂單明細是一起保存的,也就是說兩者可以作爲一個整體來看待(這個整體就是我們下面要說的聚合)
  • 而產品和產品評論之間並不能被看做一個整體,所以沒有在一起進行操作

這層邏輯,你光看上面的設計是看不出來的,只有看到代碼了,才能理清這一層關係。這無形中就增加了理解和使用難度。「聚合」就是緩解這種問題的一種手段!

什麼是聚合和聚合根?

在討論聚合之前,我們先來看一段Java代碼:

public class People {
 public void say() {
 System.out.println("1");
 System.out.println("2");
 }
}

對於上面的代碼,如何保障在多線程情況下1和2能按順序打印出來?最簡單的方法就是使用synchronized關鍵字進行加鎖操作,像這樣:

public class People {
 public synchronized void say() {
 System.out.println("1");
 System.out.println("2");
 }
}

synchronized保證了代碼的原子性執行。與之類似的就是事務,在JDBC的架構設計中已經聊過了事務,這裏不再贅述。事務保證了原子性操作。

但是,這和「聚合」有什麼關係呢?

如果說,synchronized是多線程層面的鎖;事務是數據庫層面的鎖,那麼「聚合」就是業務層面的鎖!

在業務邏輯上,有些對象需要保持操作上的原子性,否則就沒有任何意義。這些對象就組成了「聚合」!

對於上面的訂單與訂單詳情,從業務上來看,訂單與訂單明細需要保持業務上的原子性操作:

  • 訂單必須要包含訂單明細
  • 訂單明細必須要屬於某個訂單
  • 訂單和訂單明細被視爲一個整體,少了任何一個都沒有意義

所以其對象模型可以表示爲:

領域設計:聚合與聚合根

 

  • 訂單和訂單明細組成一個「聚合」
  • 訂單是操作的主體,所以訂單是這個「聚合」的「聚合根」
  • 所有對這個「聚合」的操作,只能通過「聚合根」進行

相應的,產品和產品評價就不構成「聚合」。雖然在表設計時,訂單和訂單明細的結構關係與產品與產品評價的結構關係是一樣的!因爲:

  • 雖然產品評價需要屬於某個產品
  • 但是產品不一定就有產品評價
  • 產品評價可以獨立操作

所以產品與產品評論的模型則可以表示爲:

領域設計:聚合與聚合根

 

  • 產品和產品評論是兩個「聚合」
  • 產品評論通過productId與「產品聚合」進行關聯

如何確定聚合和聚合根?

對象在業務邏輯上是否需要保證原子性操作是確定聚合和聚合根的其中一個約束。還有一個約束就是「邊界」,即聚合多大才合適?過大的「聚合」會帶來各種問題。

還以鎖舉例,看下面的代碼:

public class People {
 public synchronized void say() {
 System.out.println("0");
 System.out.println("1");
 System.out.println("2");
 System.out.println("4");
 }
}

如果我只希望12能按順序打印出來,而0和4沒有這個要求!上面的代碼能滿足要求,但是影響了性能。優化方式是使用同步塊,縮小同步範圍:

public class People {
 public void say() {
 System.out.println("0");
 synchronized(Locker.class){
 System.out.println("1");
 System.out.println("2");
 }
 System.out.println("4");
 }
}

「邊界」就像上面的同步塊一樣,只將需要的對象組合成聚合!

假設上面的產品和產品評論構成了一個聚合!那會發生什麼事情呢?當A,B兩個用戶同時對這個商品進行評論,A先開始評論,此時就會鎖定該產品對象以及下面的所有評論,在A提交評論之前,B是無法操作這個產品對象的,顯然這是不合理的。

Respository與DAO的區別

在理解了聚合之後,就可以很容易的區分Respository與DAO了:

  • DAO是技術手段,Respository是抽象方式
  • DAO只是針對對象的操作,而Respository是針對「聚合」的操作

DAO的操作方式如下:

@Service
@Transactional
public class OrderService {
 public void createOrder(Order order,List<OrderDetail> orderDetailList) throws Exception {
 Long orderId = orderDao.save(order);
 for(OrderDetail detail : orderDetailList) {
 detail.setOrderId(orderId);
 orderDetailDao.save(detail);
 }
 }
 }
}
  • 訂單和和訂單明細都有一個對應的DAO
  • 訂單和訂單明細的關係並沒有在對象之間得到體現

而Respository的操作方式如下:

// 訂單和訂單明細構成聚合
Order{
 List<OrderDetail> itemLine; // 這裏就保證了設計與編碼的一致性
 ...
}
@Service
@Transactional
public class OrderService {
 public void createOrder(Order order) throws Exception {
 orderRespository.save(order);
 //or
 order.save(); // 內部調用orderRespository.save(this);
 }
}

當然,orderRespository的save方法中,可能還是數據庫相關操作,但也可能是NoSql操作甚至內存操作。

參考資料

  • 《領域驅動設計:軟件核心複雜性應對之道》
  • 《實現領域驅動設計》

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