領域設計:聚合與聚合根

本文試圖回答以下問題:程序員

  • 什麼是聚合?
  • 什麼是聚合根?
  • 如何肯定聚合和聚合根?
  • 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之間是一對多的關係。this

那麼,請問: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操做甚至內存操做。

參考資料

  • 《領域驅動設計:軟件核心複雜性應對之道》
  • 《實現領域驅動設計》
相關文章
相關標籤/搜索