《領域驅動設計之PHP實現》- 工廠

工廠

工廠是強大的抽象。它們有助於使客戶端在如何與領域交互的細節裏解耦。客戶端不須要了解怎麼構建複雜對象和聚合,因此你能夠用工廠建立整個聚合,從而保證它們的不變性。php

聚合根上的工廠方法

工廠方法(Factory Method)模式,正如經典著做《Gang of Four》中所述,是一個建立型模式:app

它定義一個建立對象的接口,但將對象類型的選擇權交給其子類,建立被延遲到運行時。工具

在聚合根裏增一個工廠方法隱藏了從外部客戶端建立聚合的內部實現細節。這也將集成聚合的責任轉移到根。post

在一個含有 User 實體和 Wish 實體的領域模型內,User 扮演了聚合根的角色。不存在無 User 的 Wish。User 實體應該管理它的聚合。單元測試

把對 Wish 的控制轉移到 User 實體的辦法就是經過在聚合根內放置一個工廠方法:測試

class User
{
// ...
    public function makeWish(WishId $wishId, $email, $content)
    {
        $wish = new WishEmail(
            $wishId,
            $this->id(),
            $email,
            $content
        );
        DomainEventPublisher::instance()->publish(
            new WishMade($wishId)
        );
        return $wish;
    }
}

客戶端不須要了解內部細節,即聚合根是如何處理建立細節的:ui

$wish = $aUser->makeWish(
    $wishRepository->nextIdentity(),
    'user@example.com',
    'I want to be free!'
);

強一致性

聚合根內的工廠方法也是不變性的好地方。this

在一個含有 Forum(論壇) 和 Post(帖子) 實體的領域模型裏,Post 聚合是聚合根 Forum 的一部分,發佈一個 Post 就像這樣:spa

class Forum
{
// ...
    public function publishPost(PostId $postId, $content)
    {
        $post = new Post($this->id, $postId, $content);
        DomainEventPublisher::instance()->publish(
            new PostPublished($postId)
        );
        return $post;
    }
}

在與領域專家溝通後,咱們得知當 Forum 關閉後就不能建立 Post。這是一個不變量,而且咱們能夠在建立 Post 時強制這樣,從而避免領域狀態的不一致。代理

class Forum
{
// ...
    public function publishPost(PostId $postId, $content)
    {
        if ($this->isClosed()) {
            throw new ForumClosedException();
        }
        $post = new Post($this->id, $postId, $content);
        DomainEventPublisher::instance()->publish(
            new PostPublished($postId)
        );
        return $post;
    }
}

服務內的工廠(Factory on Service)

解耦建立邏輯也能夠在咱們的服務內派上用場。

構建規格 (Building Spefifications)

在服務內使用規格多是最好的例子來講明怎樣在服務內使用工廠。

考慮下面的服務例子。給定一個外部的請求,咱們想基於最新的 Posts 添加到系統時構建一個反饋(feed):

namespace Application\Service;

use Domain\Model\Post;
use Domain\Model\PostRepository;

class LatestPostsFeedService
{
    private $postRepository;

    public function __construct(PostRepository $postRepository)
    {
        $this->postRepository = $postRepository;
    }

    /**
     * @param LatestPostsFeedRequest $request
     */
    public function execute($request)
    {
        $posts = $this->postRepository->latestPosts($request->since);
        return array_map(function (Post $post) {
            return [
                'id' => $post->id()->id(),
                'content' => $post->body()->content(),
                'created_at' => $post->createdAt()
            ];
        }, $posts);
    }
}

倉儲裏的 Finder 方法(例如 latestPosts)有一些限制,由於它們無限制地持續增長複雜性到咱們的倉儲中。正如咱們在第 10 章,倉儲 中討論的,規格是一個更好的方法。

咱們很幸運,在 PostRepository 中咱們有一個與規格工做很好的 query 方法:

class LatestPostsFeedService
{
// ...
    public function execute($request)
    {

        $posts = $this->postRepository->query($specification);
    }
}

對規格使用一個具體的實現是一個壞主意:

class LatestPostsFeedService
{
    public function execute($request)
    {
        $posts = $this->postRepository->query(
            new SqlLatestPostSpecification($request->since)
        );
    }
}

將高層次的應用服務和將層次的規格實現耦合在一塊兒,會混合層並破壞關注點分離(Separation of Concerns)。除此以外,將服務耦合到具體基礎設施實現中也是至關壞的方式。你沒法在 SQL 持久化解決方案以外使用此服務。若是咱們想經過內存實現來測試咱們的服務怎麼辦?

問題的解決方案就是,經過使用抽象工廠模式(Abstract Factory pattern)從服務自己解耦規格的建立。依據 OODesign.com

抽象工廠提供建立一系列相關對象的接口,而不用顯式指定他們的類。

由於咱們有了多種規格實現,咱們首先須要爲工廠建立一個接口:

namespace Domain\Model;
interface PostSpecificationFactory
{
    public function createLatestPosts(DateTimeImmutable $since);
}

而後咱們須要爲每一個 PostRepository 實現建立工廠。做爲一個例子,對於內存中的 PostRepository 實現就像這樣:

namespace Infrastructure\Persistence\InMemory;
use Domain\Model\PostSpecificationFactory;
class InMemoryPostSpecificationFactory
    implements PostSpecificationFactory
{
    public function createLatestPosts(DateTimeImmutable $since)
    {
        return new InMemoryLatestPostSpecification($since);
    }
}

一旦咱們有一個用來放置建立邏輯的中心位置,那麼就很容易從服務中解耦:

class LatestPostsFeedService
{
    private $postRepository;
    private $postSpecificationFactory;
    public function __construct(
        PostRepository $postRepository,
        PostSpecificationFactory $postSpecificationFactory
    ) {
        $this->postRepository = $postRepository;
        $this->postSpecificationFactory = $postSpecificationFactory;
    }
    public function execute($request)
    {
        $posts = $this->postRepository->query(
            $this->postSpecificationFactory->createLatestPosts(
                $request->since
            )
        );
    }
}

如今,經過內存中的 PostRepository 實現對咱們的服務進行單元測試很是容易:

namespace Application\Service;

use Domain\Model\Body;
use Domain\Model\Post;
use Domain\Model\PostId;
use Infrastructure\Persistence\InMemory\InMemoryPostRepositor;

class LatestPostsFeedServiceTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var \Infrastructure\Persistence\InMemory\InMemoryPostRepository
     */
    private $postRepository;
    /**
     * @var LatestPostsFeedService
     */
    private $latestPostsFeedService;

    public function setUp()
    {
        $this->latestPostsFeedService = new LatestPostsFeedService(
            $this->postRepository = new InMemoryPostRepository()
        );
    }

    /**
     * @test
     */
    public function shouldBuildAFeedFromLatestPosts()
    {
        $this->addPost(1, 'first', '-2 hours');
        $this->addPost(2, 'second', '-3 hours');
        $this->addPost(3, 'third', '-5 hours');
        $feed = $this->latestPostsFeedService->execute(
            new LatestPostsFeedRequest(
                new \DateTimeImmutable('-4 hours')
            )
        );
        $this->assertFeedContains([
            ['id' => 1, 'content' => 'first'],
            ['id' => 2, 'content' => 'second']
        ], $feed);
    }

    private function addPost($id, $content, $createdAt)
    {
        $this->postRepository->add(new Post(
            new PostId($id),
            new Body($content),
            new \DateTimeImmutable($createdAt)
        ));
    }

    private function assertFeedContains($expected, $feed)
    {
        foreach ($expected as $index => $contents) {
            $this->assertArraySubset($contents, $feed[$index]);
            $this->assertNotNull($feed[$index]['created_at']);
        }
    }
}

構建聚合

實體對持久化機制是不可知的。你不想用持久化細節耦合和污染你的實體。看一下下面的應用服務:

class SignUpUserService
{
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @param SignUpUserRequest $request
     */
    public function execute($request)
    {
        $email = $request->email();
        $password = $request->password();
        $user = $this->userRepository->userOfEmail($email);
        if (null !== $user) {
            throw new UserAlreadyExistsException();
        }
        $this->userRepository->persist(new User(
            $this->userRepository->nextIdentity(),
            $email,
            $password
        ));
        return $user;
    }
}

想象以下的一個 User 實體:

class User
{
    private $userId;
    private $email;
    private $password;
    public function __construct(UserId $userId, $email, $password)
    {
// ...
    }
// ...
}

假定咱們想用 Doctrine 做爲咱們的基礎持久化機制。Doctrine 須要一個普通字符串變量的 id 來保證工做正常。在咱們的實體中,$userId 是一個 UserId 值對象。添加一個額外的 id 到 User 實體僅僅是由於 Doctrine 會將咱們的領域模型和持久化機制耦合。咱們在第 4 章,實體 裏看到,咱們能夠用一個代理(Surrogate) ID 解決這個問題,即經過在基礎設施層的 User 實體外圍建立一個 wrapper:

class DoctrineUser extends User
{
    private $surrogateUserId;
    public function __construct(UserId $userId, $email, $password)
    {
        parent:: __construct($userId, $email, $password);
        $this->surrogateUserId = $userId->id();
    }
}

由於在應用服務中建立 DoctrineUser 會再次耦合持久層和領域,咱們須要用抽象工廠在服務外解耦這個建立邏輯。

咱們能夠經過在領域內建立一個接口:

interface UserFactory
{
    public function build(UserId $userId, $email, $password);
}

而後,咱們把它的實現放置到基礎設施層:

class DoctrineUserFactory implements UserFactory
{
    public function build(UserId $userId, $email, $password)
    {
        return new DoctrineUser($userId, $email, $password);
    }
}

只要解耦,咱們僅僅須要把工廠注入到應用服務內:

class SignUpUserService
{
    private $userRepository;
    private $userFactory;

    public function __construct(
        UserRepository $userRepository,
        UserFactory $userFactory
    )
    {
        $this->userRepository = $userRepository;
        $this->userFactory = $userFactory;
    }

    /**
     * @param SignUpUserRequest $request
     */
    public function execute($request)
    {
// ...
        $user = $this->userFactory->build(
            $this->userRepository->nextIdentity(),
            $email,
            $password
        );
        $this->userRepository->persist($user);
        return $user;
    }
}

測試工廠

當你在寫測試時,會看到一個通用模式。這是由於建立實體和複雜聚合是一個很是繁瑣且重複的過程,複雜性和重複性將開始滲透到你的測試套件裏。考慮如下實體:

class Author
{
    private $username;
    private $email ;
    private $fullName;
    public function __construct(
        Username $aUsername,
        FullName $aFullName,
        Email $anEmail
    ) {
        $this->username = $aUsername;
        $this->email = $anEmail ;
        $this->fullName = $aFullName;
    }
// ...
}

在系統中的某處,你將獲得以下所示的測試:

class MyTest extends PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function itDoesSomething()
    {
        $author = new Author(
            new Username('johndoe'),
            new FullName('John', 'Doe' ),
            new Email('john@doe.com' )
        );
//do something with author
    }
}

邊界內的服務分享像實體,聚合,和值對象這些概念。想象一下在整個測試中一遍又一遍地重複相同的構建邏輯的混亂狀況。正如咱們將看到的,從測試中提取構建邏輯很是方便,而且能夠防止重複。

母體對象(Object Mother)

母體對象是工廠的易記名稱,它爲測試建立固定的夾具。與前面的示例相似,咱們能夠將重複的邏輯提取到母體對象,以即可以在測試之間重用:

class AuthorObjectMother
{
    public static function createOne()
    {
        return new Author(
            new Username('johndoe'),
            new FullName('John', 'Doe'),
            new Email('john@doe.com')
        );
    }
}

class MyTest extends PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function itDoesSomething()
    {
        $author = AuthorObjectMother::createOne();
    }
}

你會注意到,你有越多的測試和場景,工廠就會有越多的方法。

由於母體對象不太靈活,它們的複雜性每每會迅速增加。幸運的是,這裏有更靈活的測試方法。

測試數據構建器(Test Data Builder)

測試數據構建器只是普通的構建器,其默認值專用於測試套件,所以你沒必要在特定的測試用例上指定不相關的參數:

class AuthorBuilder
{
    private $username;
    private $email;
    private $fullName;

    private function __construct()
    {
        $this->username = new Username('johndoe');
        $this->email = new Email('john@doe.com');
        $this->fullName = new FullName('John', 'Doe');
    }

    public static function anAuthor()
    {
        return new self();
    }

    public function withFullName(FullName $aFullName)
    {
        $this->fullName = $aFullName;
        return $this;
    }

    public function withUsername(Username $aUsername)
    {
        $this->username = $aUsername;
        return $this;
    }

    public function withEmail(Email $anEmail)
    {
        $this->email = $anEmail;
        return $this;
    }

    public function build()
    {
        return new Author($this->username, $this->fullName, $this->email);
    }
}

class MyTest extends PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function itDoesSomething()
    {
        $author = AuthorBuilder::anAuthor()
            ->withEmail(new Email('other@email.com'))
            ->build();
    }
}

咱們甚至能夠結合使用測試數據構建器來構建更復雜的聚合,好比 Post:

class Post
{
    private $id;
    private $author;
    private $body;
    private $createdAt;
    public function __construct(
        PostId $anId, Author $anAuthor, Body $aBody
    ) {
        $this->id = $anId;
        $this->author = $anAuthor;
        $this->body = $aBody;
        $this->createdAt = new DateTimeImmutable();
    }
}

讓咱們看看 Post 相應的測試數據構建器。咱們能夠重用 AuthorBuilder 來構建一個默認的 Author:

class PostBuilder
{
    private $postId;
    private $author;
    private $body;

    private function __construct()
    {
        $this->postId = new PostId();
        $this->author = AuthorBuilder::anAuthor()->build();
        $this->body = new Body('Post body');
    }

    public static function aPost()
    {
        return new self();
    }

    public function withAuthor(Author $anAuthor)
    {
        $this->author = $anAuthor;
        return $this;
    }

    public function withPostId(PostId $aPostId)
    {
        $this->postId = $aPostId;
        return $this;
    }

    public function withBody(Body $body)
    {
        $this->body = $body;
        return $this;
    }

    public function build()
    {
        return new Post($this->postId, $this->author, $this->body);
    }
}

如今這個解決方案對於覆蓋任何測試已經足夠靈活,包含測試內部實體構建的可能性:

class MyTest extends PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function itDoesSomething()
    {
        $post = PostBuilder::aPost()
            ->withAuthor(AuthorBuilder::anAuthor()
                ->withUsername(new Username('other'))
                ->build())
            ->withBody(new Body('Another body'))
            ->build();
//do something with the post
    }
}

小結

工廠是從咱們業務邏輯中解耦結構邏輯的強大工具。工廠方法模式不只有助於從聚合根內移除建立職責,同時強制保持領域的不變性。在服務中使用抽象工廠模式使咱們將基礎建立細節與領域邏輯分享。一個常見用例就是規格及其各自的持久化實現。咱們已經看到工廠也能夠在咱們的測試套件中派上用場。儘管咱們能夠將構建邏輯提取到母體對象工廠中,可是測試數據構建器爲咱們的測試提供了更大的靈活性。

相關文章
相關標籤/搜索