13

工厂

  1. 《领域驱动设计之PHP实现》全书翻译 - DDD入门
  2. 《领域驱动设计之PHP实现》全书翻译 - 架构风格
  3. 《领域驱动设计之PHP实现》全书翻译 - 值对象
  4. 《领域驱动设计之PHP实现》全书翻译 - 实体
  5. 《领域驱动设计之PHP实现》全书翻译 - 服务
  6. 《领域驱动设计之PHP实现》全书翻译 - 领域事件
  7. 《领域驱动设计之PHP实现》全书翻译 - 模块
  8. 《领域驱动设计之PHP实现》全书翻译 - 聚合
  9. 《领域驱动设计之PHP实现》全书翻译 - 工厂
  10. 《领域驱动设计之PHP实现》全书翻译 - 仓储
  11. 《领域驱动设计之PHP实现》全书翻译 - 应用服务
  12. 《领域驱动设计之PHP实现》全书翻译 - 集成上下文
  13. 《用PHP实现六边形架构》

工厂是强大的抽象。它们有助于使客户端在如何与领域交互的细节里解耦。客户端不需要了解怎么构建复杂对象和聚合,所以你可以用工厂创建整个聚合,从而保证它们的不变性。

聚合根上的工厂方法

工厂方法(Factory Method)模式,正如经典著作《Gang of Four》中所述,是一个创建型模式:

它定义一个创建对象的接口,但将对象类型的选择权交给其子类,创建被延迟到运行时。

在聚合根里增一个工厂方法隐藏了从外部客户端创建聚合的内部实现细节。这也将集成聚合的责任转移到根。

在一个含有 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;
    }
}

客户端不需要了解内部细节,即聚合根是如何处理创建细节的:

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

强一致性

聚合根内的工厂方法也是不变性的好地方。

在一个含有 Forum(论坛) 和 Post(帖子) 实体的领域模型里,Post 聚合是聚合根 Forum 的一部分,发布一个 Post 就像这样:

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
    }
}

小结

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


捷叔叔
1.1k 声望1.8k 粉丝