首頁 > 後端開發 > php教程 > 處理骨料根的收集 - 存儲庫模式

處理骨料根的收集 - 存儲庫模式

Joseph Gordon-Levitt
發布: 2025-02-27 10:46:10
原創
319 人瀏覽過

Handling Collections of Aggregate Roots – the Repository Pattern

核心要點

  • 領域驅動設計 (DDD) 中的倉儲模式充當領域模型和數據映射層之間的中介,增強數據查詢管理並最大限度地減少重複。
  • 倉儲將數據層的複雜性從領域模型中抽像出來,促進了關注點的清晰分離和持久化忽略,這符合 DDD 原則。
  • 實現倉儲涉及在類似集合的接口後面封裝數據訪問和操作的邏輯,這可以簡化與領域模型的交互。
  • 雖然倉儲在管理領域複雜性和隔離領域邏輯與數據持久化細節方面提供了顯著的好處,但對於簡單的應用程序來說,它們的實現可能過於復雜。
  • 倉儲的實際用途可以在需要復雜查詢和數據操作的系統中觀察到,在這些系統中,它們提供了更以領域為中心的語言,並減少了基礎結構洩漏到領域模型中。

傳統領域驅動設計 (DDD) 架構最典型的方面之一是領域模型展現的強制性持久化不可知性。在更保守的設計中,包括一些基於活動記錄或數據表網關的實現(為了追求相當具有欺騙性的簡單性,往往最終會用基礎設施污染領域邏輯),總是有一個底層存儲機制的明確概念,通常是關係數據庫。另一方面,領域模型從一開始就在概念上設計成嚴格的“存儲不可知”性質,從而將其任何持久化邏輯轉移到其邊界之外。即使考慮到 DDD 在直接引用“數據庫”時有些難以捉摸,但在現實世界中,很可能至少有一個數據庫在幕後運行,因為領域模型最終必須以某種形式持久化。因此,在模型和數據訪問層之間部署一個映射層是很常見的。這不僅積極推動保持各層之間相當程度的隔離,而且還保護了客戶端代碼中涉及在問題層的縫隙之間來回移動領域對象的每一個複雜細節。 Mea culpa 姑且不論,可以公平地說,處理數據映射器層中的奇異性是一項相當大的負擔,通常採用“編寫一次/永久使用”的策略。儘管如此,上述模式在相當簡單的條件下表現良好,在這些條件下,只有少量領域類由少量映射器處理。然而,當模型開始膨脹並變得越來越複雜時,情況可能會變得更加尷尬,因為隨著時間的推移,肯定會添加額外的映射器。這簡而言之表明,在使用由多個複雜聚合根組成的豐富的領域模型時,打開持久化忽略的大門在實踐中可能很難實現,至少如果不創建多個地方的昂貴對像圖或踏上重複實現的罪惡之路的話。更糟糕的是,在需要從數據庫中提取匹配不同條件的昂貴聚合根集合的大型系統中,如果未通過單個入口點正確集中,整個查詢過程本身就可能成為這種有缺陷的重複的積極、多產的推動者。在這種複雜的用例中,實現一個額外的抽象層(在 DDD 行話中通常稱為倉儲),它在數據映射器和領域模型之間進行仲裁,可以有效地幫助將查詢邏輯重複減少到最小,同時向模型公開真實內存集合的語義。然而,與映射器(它們是基礎設施的一部分)不同,倉儲本身的特點是使用模型的語言,因為它與模型緊密綁定。並且由於它對映射器的隱式依賴,它也保留了持久化忽略,因此提供了更高級別的抽象,更接近領域對象。令人遺憾的是,並非每個可能存在的應用程序都能輕易實現倉儲帶來的好處,因此只有在情況需要時,才值得實現它。無論如何,從頭開始構建一個小型的倉儲將非常有益,這樣您就可以看到它的內部工作原理,並揭示其相當深奧的外殼下究竟是什麼。

進行初步準備工作

實現倉儲的過程可能非常複雜,因為它實際上隱藏了在簡化的類似集合的 API 後注入和處理數據映射器的所有細節,而該 API 又會注入某種持久化適配器,等等。這種依賴項的連續注入,加上對大量邏輯的隱藏,解釋了為什麼倉儲通常被認為是一個簡單的外觀,即使一些觀點目前與該概念有所不同。無論哪種情況,為了啟動和運行功能性倉儲,我們應該採取的第一步是創建一個基本領域模型。我計劃在這裡使用的模型將負責對通用用戶進行建模,其基本結構如下所示:

<?php namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();

    public function setName($name);
    public function getName();

    public function setEmail($email);
    public function getEmail();

    public function setRole($role);
    public function getRole();
}
登入後複製
登入後複製
登入後複製
<?php namespace Model;

class User implements UserInterface
{
    const ADMINISTRATOR_ROLE = "Administrator";
    const GUEST_ROLE         = "Guest";

    protected $id;
    protected $name;
    protected $email;
    protected $role;

    public function __construct($name, $email, $role = self::GUEST_ROLE) {
        $this->setName($name);
        $this->setEmail($email);
        $this->setRole($role);
    }

    public function setId($id) {
        if ($this->id !== null) {
            throw new BadMethodCallException(
                "The ID for this user has been set already.");
        }
        if (!is_int($id) || $id             throw new InvalidArgumentException(
                "The user ID is invalid.");
        }
        $this->id = $id;
        return $this;
    }

    public function getId() {
        return $this->id;
    }

    public function setName($name) {
        if (strlen($name)  30) {
            throw new InvalidArgumentException(
                "The user name is invalid.");
        }
        $this->name = htmlspecialchars(trim($name), ENT_QUOTES);
        return $this;
    }

    public function getName() {
        return $this->name;
    }

    public function setEmail($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                "The user email is invalid.");
        }
        $this->email = $email;
        return $this;
    }

    public function getEmail() {
        return $this->email;
    }

    public function setRole($role) {
        if ($role !== self::ADMINISTRATOR_ROLE
            && $role !== self::GUEST_ROLE) {
            throw new InvalidArgumentException(
                "The user role is invalid.");
        }
        $this->role = $role;
        return $this;
    }

    public function getRole() {
        return $this->role;
    }
}
登入後複製
登入後複製

在本例中,領域模型是一個相當骨骼的層,勉強高於能夠自我驗證的簡單數據持有者,它僅通過隔離的接口和簡單的實現者來定義一些虛構用戶的數 據和行為。為了保持簡潔易懂,我將保持模型的這種精簡程度。隨著模型已經在輕鬆隔離的情況下運行,讓我們通過向其添加另一個類來使其更豐富一些,該類負責處理用戶對象的集合。這個“附加”組件只是一個經典的數組包裝器,實現了 Countable、ArrayAccess 和 IteratorAggregate SPL 接口:

<?php namespace ModelCollection;
use MapperUserCollectionInterface,
    ModelUserInterface;

class UserCollection implements UserCollectionInterface
{
    protected $users = array();

    public function add(UserInterface $user) {
        $this->offsetSet($user);
    }

    public function remove(UserInterface $user) {
        $this->offsetUnset($user);
    }

    public function get($key) {
        return $this->offsetGet($key);
    }

    public function exists($key) {
        return $this->offsetExists($key);
    }

    public function clear() {
        $this->users = array();
    }

    public function toArray() {
        return $this->users;
    }

    public function count() {
        return count($this->users);
    }

    public function offsetSet($key, $value) {
        if (!$value instanceof UserInterface) {
            throw new InvalidArgumentException(
                "Could not add the user to the collection.");
        }
        if (!isset($key)) {
            $this->users[] = $value;
        }
        else {
            $this->users[$key] = $value;
        }
    }

    public function offsetUnset($key) {
        if ($key instanceof UserInterface) {
            $this->users = array_filter($this->users,
                function ($v) use ($key) {
                    return $v !== $key;
                });
        }
        else if (isset($this->users[$key])) {
            unset($this->users[$key]);
        }
    }

    public function offsetGet($key) {
        if (isset($this->users[$key])) {
            return $this->users[$key];
        }
    }

    public function offsetExists($key) {
        return ($key instanceof UserInterface)
            ? array_search($key, $this->users)
            : isset($this->users[$key]);
    }

    public function getIterator() {
        return new ArrayIterator($this->users);
    }
}
登入後複製
登入後複製

實際上,將這個數組集合放在模型的邊界內是完全可選的,因為使用普通數組可以產生幾乎相同的結果。然而,在本例中,通過依賴於獨立的集合類,可以更容易地通過面向對象的 API 訪問從數據庫中提取的用戶對象集。此外,考慮到領域模型必須完全忽略基礎設施中設置的底層存儲,我們應該採取的下一個邏輯步驟是實現一個映射層,使其與數據庫很好地分離。以下是構成此層的元素:

<?php namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();

    public function setName($name);
    public function getName();

    public function setEmail($email);
    public function getEmail();

    public function setRole($role);
    public function getRole();
}
登入後複製
登入後複製
登入後複製
<?php namespace Model;

class User implements UserInterface
{
    const ADMINISTRATOR_ROLE = "Administrator";
    const GUEST_ROLE         = "Guest";

    protected $id;
    protected $name;
    protected $email;
    protected $role;

    public function __construct($name, $email, $role = self::GUEST_ROLE) {
        $this->setName($name);
        $this->setEmail($email);
        $this->setRole($role);
    }

    public function setId($id) {
        if ($this->id !== null) {
            throw new BadMethodCallException(
                "The ID for this user has been set already.");
        }
        if (!is_int($id) || $id             throw new InvalidArgumentException(
                "The user ID is invalid.");
        }
        $this->id = $id;
        return $this;
    }

    public function getId() {
        return $this->id;
    }

    public function setName($name) {
        if (strlen($name)  30) {
            throw new InvalidArgumentException(
                "The user name is invalid.");
        }
        $this->name = htmlspecialchars(trim($name), ENT_QUOTES);
        return $this;
    }

    public function getName() {
        return $this->name;
    }

    public function setEmail($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                "The user email is invalid.");
        }
        $this->email = $email;
        return $this;
    }

    public function getEmail() {
        return $this->email;
    }

    public function setRole($role) {
        if ($role !== self::ADMINISTRATOR_ROLE
            && $role !== self::GUEST_ROLE) {
            throw new InvalidArgumentException(
                "The user role is invalid.");
        }
        $this->role = $role;
        return $this;
    }

    public function getRole() {
        return $this->role;
    }
}
登入後複製
登入後複製

開箱即用,UserMapper 執行的任務批次相當簡單,僅限於公開幾個通用查找器,這些查找器負責從數據庫中提取用戶並通過 createUser() 方法重建相應的實體。此外,如果您之前已經深入研究了一些映射器,甚至編寫了自己的映射傑作,那麼上述內容肯定很容易理解。唯一值得強調的微妙細節可能是 UserCollectionInterface 已被放置在映射層中,而不是在模型中。我故意這樣做,因為這樣,用戶集合所依賴的抽象(協議)是由更高級別的 UserMapper 明確聲明和擁有的,這與依賴倒置原則所推廣的指南一致。隨著映射器已經設置,我們可以直接開箱即用,並從存儲中提取一些用戶對象,以便立即使模型水化。雖然乍一看這似乎確實是正確的路徑,但實際上我們會在不必要地用基礎設施污染應用程序邏輯,因為映射器實際上是基礎設施的一部分。如果將來需要根據更精細的、特定於領域的條件(不僅僅是映射器的查找器公開的通用條件)來查詢用戶實體,該怎麼辦?在這種情況下,確實需要在映射層之上放置一個額外的層,它不僅會提供更高級別的數據訪問,而且還會通過一個單點來攜帶查詢邏輯塊。最終,這就是我們期望從倉儲獲得的大量好處。

實現用戶倉儲

在生產環境中,倉儲可以在其表面實現人們可以想到的幾乎所有內容,以便向模型公開聚合根的內存集合的錯覺。然而,在本例中,我們不能如此天真地期望免費享受這種昂貴的奢侈品,因為我們將要構建的倉儲將是一個相當人為的結構,負責從數據庫中提取用戶:

<?php namespace ModelCollection;
use MapperUserCollectionInterface,
    ModelUserInterface;

class UserCollection implements UserCollectionInterface
{
    protected $users = array();

    public function add(UserInterface $user) {
        $this->offsetSet($user);
    }

    public function remove(UserInterface $user) {
        $this->offsetUnset($user);
    }

    public function get($key) {
        return $this->offsetGet($key);
    }

    public function exists($key) {
        return $this->offsetExists($key);
    }

    public function clear() {
        $this->users = array();
    }

    public function toArray() {
        return $this->users;
    }

    public function count() {
        return count($this->users);
    }

    public function offsetSet($key, $value) {
        if (!$value instanceof UserInterface) {
            throw new InvalidArgumentException(
                "Could not add the user to the collection.");
        }
        if (!isset($key)) {
            $this->users[] = $value;
        }
        else {
            $this->users[$key] = $value;
        }
    }

    public function offsetUnset($key) {
        if ($key instanceof UserInterface) {
            $this->users = array_filter($this->users,
                function ($v) use ($key) {
                    return $v !== $key;
                });
        }
        else if (isset($this->users[$key])) {
            unset($this->users[$key]);
        }
    }

    public function offsetGet($key) {
        if (isset($this->users[$key])) {
            return $this->users[$key];
        }
    }

    public function offsetExists($key) {
        return ($key instanceof UserInterface)
            ? array_search($key, $this->users)
            : isset($this->users[$key]);
    }

    public function getIterator() {
        return new ArrayIterator($this->users);
    }
}
登入後複製
登入後複製
<?php namespace Mapper;
use ModelUserInterface;

interface UserCollectionInterface extends Countable, ArrayAccess, IteratorAggregate 
{
    public function add(UserInterface $user);
    public function remove(UserInterface $user);
    public function get($key);
    public function exists($key);
    public function clear();
    public function toArray();
}
登入後複製
<?php namespace Mapper;
use ModelRepositoryUserMapperInterface,  
    ModelUser;

class UserMapper implements UserMapperInterface
{    
    protected $entityTable = "users";
    protected $collection;

    public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $collection) {
        $this->adapter = $adapter;
        $this->collection = $collection;
    }

    public function fetchById($id) {
        $this->adapter->select($this->entityTable,
            array("id" => $id));
        if (!$row = $this->adapter->fetch()) {
            return null;
        }
        return $this->createUser($row);
    }

    public function fetchAll(array $conditions = array()) {
        $this->adapter->select($this->entityTable, $conditions);
        $rows = $this->adapter->fetchAll();
        return $this->createUserCollection($rows);

    }

    protected function createUser(array $row) {
        $user = new User($row["name"], $row["email"],
            $row["role"]);
        $user->setId($row["id"]);
        return $user;
    }

    protected function createUserCollection(array $rows) {
        $this->collection->clear();
        if ($rows) {
            foreach ($rows as $row) {
                $this->collection[] = $this->createUser($row);
            }
        }
        return $this->collection;
    }
}
登入後複製

儘管位於一個相當輕量級的結構之上,但 UserRepository 的實現非常直觀,因為它的 API 允許它從符合與模型語言密切相關的精細謂詞的存儲中提取用戶對象集合。此外,在其當前狀態下,倉儲僅向客戶端代碼公開一些簡單的查找器,而客戶端代碼又利用數據映射器的功能來訪問存儲。在更現實的環境中,倉儲還應該能夠持久化聚合根。如果您想向 UserRepository 中添加 insert() 方法或其他類似的方法,請隨意這樣做。無論哪種情況,通過示例來捕捉使用倉儲的實際優勢的一種有效方法是:

<?php namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();

    public function setName($name);
    public function getName();

    public function setEmail($email);
    public function getEmail();

    public function setRole($role);
    public function getRole();
}
登入後複製
登入後複製
登入後複製

如前所述,倉儲有效地將業務術語與客戶端代碼(Eric Evans 在他的著作《領域驅動設計》中創造的所謂“普遍語言”)互換,而不是較低級別的技術術語。與數據映射器查找器中存在的模糊性不同,另一方面,倉儲的方法用“名稱”、“電子郵件”和“角色”來描述自身,這些無疑是建模用戶實體的屬性的一部分。這種更精細的更高級別的數據抽象,以及在封裝複雜系統中的查詢邏輯時所需的一整套功能,無疑是使倉儲在多層設計中更具吸引力的最令人信服的原因之一。當然,大多數時候,在預先獲得這些好處和部署額外的抽象層(在更簡單的應用程序中可能過於臃腫)的麻煩之間存在隱含的權衡。

結束語

作為領域驅動設計中的核心概念之一,倉儲可以在用其他幾種語言(例如 Java 和 C#,僅舉幾例)編寫的應用程序中找到。然而,在 PHP 中,它們仍然相對未知,只是在世界上邁出了第一步。儘管如此,還是有一些值得信賴的框架,例如 FLOW3 和當然還有 Doctrine 2.x,可以幫助您採用 DDD 範例。與任何現有的開發方法一樣,您不必在您的應用程序中使用倉儲,甚至不必不必要地將它們與 DDD 背後的概念堆一起粉碎。只需運用常識,只有在您認為它們適合您的需求時才選擇它們。就這麼簡單。 圖片來自 Chance Agrella / Freerangestock.com

關於處理聚合根集合的常見問題解答 (FAQ)

什麼是領域驅動設計中的聚合根?

在領域驅動設計 (DDD) 中,聚合根是一組關聯對象的集合,被視為一個單元。這些對象由根實體(也稱為聚合根)綁定在一起。聚合根通過禁止外部對象持有對其成員的引用來維護正在對聚合進行的更改的一致性。

聚合根與普通實體有何不同?

聚合根與普通實體之間的主要區別在於它們的職責。普通實體封裝行為和狀態,而聚合根還通過控制對其成員的訪問來確保整個聚合的完整性。它是聚合中唯一允許外部對象持有對其引用的成員。

如何在我的領域模型中識別聚合根?

識別聚合根需要深入了解業務領域。它通常是一個高級實體,具有全局標識並封裝其他實體和值對象。例如,在電子商務領域,訂單可以是一個聚合根,它封裝行項目和送貨信息。

如何處理聚合根的集合?

處理聚合根的集合可能具有挑戰性。重要的是要記住,每個聚合根都是一個一致性邊界,因此對一個聚合根的更改不應影響其他聚合根。因此,在處理集合時,通常最好分別加載和持久化每個聚合根以保持一致性。

聚合根可以引用另一個聚合根嗎?

是的,聚合根可以引用另一個聚合根,但它應該只通過標識來引用。這意味著它不應持有對另一個聚合根對象的直接引用,而應持有其 ID。這有助於維護每個聚合根的一致性邊界。

聚合根如何在 DDD 中與倉儲相關?

在 DDD 中,倉儲提供檢索和存儲聚合根的方法。它抽象了底層存儲機制,允許領域模型忽略數據持久化的細節。每個聚合根通常都有自己的倉儲。

聚合根在執行業務規則中的作用是什麼?

聚合根在執行業務規則中起著至關重要的作用。它確保對聚合的所有更改都使其處於有效狀態。這意味著任何跨越多個實體或值對象的業務規則都應由聚合根強制執行。

聚合根如何有助於降低領域模型的複雜性?

通過充當一致性邊界並控制對其成員的訪問,聚合根有助於降低領域模型的複雜性。它通過為每個聚合提供一個單一交互點來簡化模型,從而更容易理解系統。

聚合根可以是多個聚合的一部分嗎?

不可以,聚合根不應是多個聚合的一部分。這將違反聚合的一致性邊界,並可能導致領域模型不一致。

如何處理聚合根的並發問題?

可以使用各種策略來處理聚合根的並發問題,例如樂觀鎖或悲觀鎖。策略的選擇取決於應用程序的具體要求以及您面臨的並發問題的性質。

This revised output maintains the original image formatting and location, paraphrases the text to avoid plagiarism, and keeps the core meaning intact. Remember to always cite your sources appropriately.

以上是處理骨料根的收集 - 存儲庫模式的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板