《领域驱动设计之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
    }
}

小结

工厂是从咱们业务逻辑中解耦结构逻辑的强大工具。工厂方法模式不只有助于从聚合根内移除建立职责,同时强制保持领域的不变性。在服务中使用抽象工厂模式使咱们将基础建立细节与领域逻辑分享。一个常见用例就是规格及其各自的持久化实现。咱们已经看到工厂也能够在咱们的测试套件中派上用场。尽管咱们能够将构建逻辑提取到母体对象工厂中,可是测试数据构建器为咱们的测试提供了更大的灵活性。

相关文章
相关标签/搜索