1. 程式人生 > >領域設計:聚合與聚合根

領域設計:聚合與聚合根

本文試圖回答如下問題:

  • 什麼是聚合?
  • 什麼是聚合根?
  • 如何確定聚合和聚合根?
  • 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操作甚至記憶體操作。

參考資料

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