在面向對象設計與 UML 建模中,「聚合」與「組合」是兩種特殊的「擁有」關係,區分它們對於領域模型準確性、生命週期管理、資料庫外鍵設計至關重要。本文從定義、UML 符號、代碼體現、業務語義、資料庫映射五個維度徹底講透。
| 維度 | 聚合 | 組合 |
|---|---|---|
| 語義強度 | 弱「擁有」:Has-a,整體與部分可分離 | 強「擁有」:Contains-a,部分不可離開整體 |
| 生命週期 | 部分獨立,可被多個整體共享 | 部分依賴整體,隨整體創建/銷毀 |
| 所有權 | 共享所有權 | 獨占所有權 |
| UML 符號 | 空心菱形 ◇── | 實心菱形 ◆── |
| 菱形位置 | 在整體類一側 | 在整體類一側 |
訂單 ◆── 訂單項、房子 ◆── 房間、人 ◆── 身份證號、部門 ◆── 內部項目。訂單刪除 → 訂單項必須刪除;房間不能獨立於房子存在。教師 ◇── 學生、汽車 ◇── 引擎(可拆卸移植)、部門 ◇── 員工(員工可調動)、圖書館 ◇── 圖書(圖書可借出/轉贈)。教師離職學生不消失;引擎可裝到另一輛車。public class Order {
private final List<OrderItem> items = new ArrayList<>(); // final,不可替換集合引用
public Order() {} // 無參構造
public void addItem(Product product, int quantity) {
// 內部創建,外部不可直接 new OrderItem
items.add(new OrderItem(product, quantity, this)); // 反向引用可選
}
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items); // 防御性拷貝/不可修改視圖
}
// 無 setItems(),無公共構造器暴露 OrderItem 創建
}
// OrderItem 無獨立生命週期,通常不單獨持久化為聚合根
@Entity
public class OrderItem {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "order_id", nullable = false) // 外鍵非空,級聯刪除
private Order order; // 關聯整體
// 業務屬性...
}
public class Department {
private final Set<Employee> employees = new HashSet<>();
public void addEmployee(Employee emp) {
employees.add(emp);
emp.setDepartment(this); // 雙向關聯維護
}
public void removeEmployee(Employee emp) {
employees.remove(emp);
emp.setDepartment(null);
}
public Set<Employee> getEmployees() {
return Collections.unmodifiableSet(employees);
}
}
@Entity
public class Employee {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "dept_id") // 可為空,員工可無部門
private Department department;
// 業務屬性...
}
@startuml
class Order {
- items: List
+ addItem()
+ getItems()
}
class OrderItem {
- order: Order
}
class Department {
- employees: Set
+ addEmployee()
+ removeEmployee()
}
class Employee {
- department: Department
}
Order *-- OrderItem : 組合 (實心菱形)
Department o-- Employee : 聚合 (空心菱形)
@enduml
| 關係 | 外鍵位置 | 級聯操作 | 可空性 | 刪除行為 |
|---|---|---|---|---|
| 組合 | 從表 | CascadeType.ALL + orphanRemoval=true | NOT NULL | 刪主表 → 級聯刪從表 |
| 聚合 | 從表 | 通常無級聯,或 PERSIST/MERGE | NULLABLE | 刪主表 → 從表外鍵置 NULL 或保留 |
// 組合:Order 刪除時自動刪除所有 OrderItem
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
// 聚合:Department 刪除不影響 Employee,僅置空 dept_id
@OneToMany(mappedBy = "department", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Employee> employees = new HashSet<>();
DDD 將「組合」關係內的整體稱為聚合根,部分稱為實體/值對象。聚合根是外部訪問的唯一入口,保證不變條件。
原則:外部只能持有聚合根引用,不可直接引用/修改內部實體。所有業務操作通過聚合根方法完成(如 order.addItem() 而非 new OrderItem())。
User ◆── Address。用戶刪除地址不應刪除(地址可能被其他用戶引用、歷史訂單保留)。正確:User ◇── Address 或值對象嵌入。Order ◇── OrderItem。導致 OrderItem 可獨立存在、被其他 Order 引用、刪除 Order 時孤兒數據殘留。department.addEmployee(emp) 必須同時設置 emp.setDepartment(this),否則關係斷裂。return items; 允許外部 items.clear() 繞過業務邏輯。必須返回 unmodifiableList 或防御性拷貝。聚合與組合的本質區別在於部分對象的生命週期是否依賴整體。組合 = 強生命週期綁定 + 獨占擁有;聚合 = 弱關聯 + 可共享 + 獨立生命週期。在代碼中通過 final 集合、無 setter、內部實例化、級聯刪除體現組合;通過可空外鍵、Setter 注入、無級聯刪除體現聚合。DDD 聚合根模式進一步強化了組合的封裝性與一致性邊界。正確建模,領域邏輯才清晰、持久化才正確、演進才可控。