本文轉載自【何以解耦】:codedecoupled.com/php-es-aggregate...
聚合是 DDD(領域驅動設計)中一個相對複雜的概念。作為 DDD 戰術設計中舉足輕重的工具,我們有必要對其瞭若指掌。
總體來說,聚合(Aggregate)是指一組緊密相關的類,他們自成一體形成一個有邊界的組織,邊界外部的物件只可以通過聚合根(Aggregate Root)與此聚合互動:
聚合常會與泛型 collection 混淆,他們的區別在於聚合是相對於領域模型而言,而 collection 是一種廣泛的概念。那麼具體來說,除了聚合根,聚合內部還有哪些類呢?本文闡述一個事件溯源的架構中,常見的幾個重要組成部分(類)。
實體
實體為獨立事物的建模工具,每個實體都擁有唯一識別符號,比如 ID
, UUID
,Username
等等。大多數情況下,實體是可變的,它的狀態會隨著時間的遷移而改變。
實體很好理解, 如果我們用 DDD 來分析 ORM 業務,那麼 ORM 中的 Model 可以理解為實體。資料庫中的每一條資料都擁有自己的 ID(唯一識別符號),每條資料的內容也會變化無常。
用實體為使用者(User)建模:
class User extends Model
{
private int $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function changePassword(string $password)
{
// domain code
}
}
$user = new User(1);
$user->changePassword();
值物件
值物件是領域中用來描述,量化或者測量實體的模型。和實體不同,值物件沒有唯一的識別符號,兩個對等的值物件是可以替換的。值物件具有不變性(Immutability),一旦建立以後,一個值物件的屬性就定型了,不可更改。
值物件是一個強大但通常被忽略的建模工具。使用值物件建模可以大大簡化我們的程式碼。下面我們來分析一下值物件幾個特性。
無識別符號性
值物件一定沒有諸如 ID
, UUID
等等之類的識別符號。
不變性
值物件初始化以後,他的值變無法被更改。我們在用值物件建模時需要特別注意,不應該給值物件賦予改變其內部屬性的功能。如果需要變異方法時,此方法必須建立一個全新的值物件(見以下 Money::add()
)。
class Money
{
private int $amountInCent;
private string $currency;
public function __construct(int $amountInCent, string $currency)
{
$this->amountInCent = $amountInCent;
$this->currency = $currency;
}
public function getAmountInCent(): int
{
return $this->amountInCent;
}
public function getCurrency(): string
{
return $this->currency;
}
public function add(Money $money): Money
{
if ($this->currency !== $this->getCurrency()) {
throw new \Exception('You can not add two different currency');
}
return new Money (
$this->amountInCent + $money->getAmountInCent(),
$this->currency
);
}
}
可替換性
值物件的不變性奠定了其可替換性的基礎,如果兩個值物件的值對等,我們可以放心地替換兩個值物件。我們可以這樣理解這個概念:如果甲有一張10塊錢人民幣鈔票,乙也有一張10塊錢人民幣鈔票,他們可以進行對等交換而不會引起利益糾紛。
領域中很多模型都可以使用值物件來建模,它的特性給予我們一個安全的,靈活的環境來管理我們的程式碼。比如上節中的 $password
就可以使用值物件來完成。
class Password
{
private string $value;
public function __construct(string $value)
{
$this->guard($value);
$this-> value = $value;
}
public function toString()
{
return $this->value;
}
private function guard($value)
{
if (empty($value)) {
throw new /Exception('invalid password value');
}
}
}
$user = new User(1);
$password = new Password($passwordStr);
$user->changePassword($password);
初始化成功後的 $password
一定是有效的,因為我們在初始化中做了防守。值物件為我們提供了一個安全的模型,接下來的程式碼中我們都無需擔心 $password
是否符合業務要求。
聚合根
聚合根的本質是實體,或者說聚合根是一種特殊的實體。聚合根的特殊性表現在它在聚合中的地位,它是聚合對外的唯一介面。如果我們把聚合想象成一個戶口簿,那麼聚合根就好像戶主,任何戶口簿的操作行為都需要通過戶主。
領域事件
領域事件是領域中與業務息息相關的業務事件,它與基礎設施,程式碼框架無關,所以領域事件不應該是諸如 EmailSent
,RecordUpdated
之類的極其廣泛的框架事件。
領域事件在聚合根中產生,然後被儲存持久化。領域事件持久化是最後一個步驟,以此來確保所有的領域事件的正確性。如有任何異常,領域事件都不會被錯誤地儲存起來。
總結
在一個事件溯源的架構中,一個聚合必定是由一個聚合根以及以上一個或多個部分組成。
本文轉載自【何以解耦】: codedecoupled.com/php-es-aggregate... ,如果你也對 TDD,DDD 以及簡潔程式碼感興趣,歡迎關注公眾號【何以解耦】,一起探索軟體開發之道。
本作品採用《CC 協議》,轉載必須註明作者和本文連結