Building Specifications

Using Specifications in our Services might be the best example to illustrate how to use Factories within our Services.

Consider the following Service example. Given a request from the outside world, we want to build a feed based on the latest Posts added to the system:

namespace ApplicationService;

use DomainModelPost;
use DomainModelPostRepository;

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 methods in Repositories like latestPosts have some limitations, as they keep adding complexity to our Repositories indefinitely. As we discuss in the Chapter 10, Repositories Specifications are a better approach.

Lucky for us, we have a nice query method in our PostRepository that works with Specifications:

class LatestPostsFeedService 
{
// ...

public function execute($request)
{
$posts = $this->postRepository->query($specification);
}
}

Using a concrete implementation for the Specification is a bad idea:

class LatestPostsFeedService
{

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

Coupling our high-level application Service with a low-level Specification implementation mixes layers and breaks the Separation of Concerns. In addition, this is a pretty bad way of coupling our Service to a concrete Infrastructure implementation. There's no way you could use this Service outside of the SQL persistence solution. What if we want to test our Service with an in-memory implementation?

The solution to this problem is to decouple Specification creation from the Service itself by using the Abstract Factory pattern. According to OODesign.com:

Abstract Factory offers the interface for creating a family of related objects, without explicitly specifying their classes.

As we might have multiple Specification implementations, we first need to create an interface for the Factory:

namespace DomainModel;

interface PostSpecificationFactory
{
public function createLatestPosts(DateTimeImmutable $since);
}

Then we need to create Factories for each PostRepository implementation. As an example, a Factory for the in-memory PostRepository implementation could look like this:

namespace InfrastructurePersistenceInMemory;

use DomainModelPostSpecificationFactory;

class InMemoryPostSpecificationFactory
implements PostSpecificationFactory
{
public function createLatestPosts(DateTimeImmutable $since)
{
return new InMemoryLatestPostSpecification($since);
}
}

Once we have a centralized place for the creation logic, it's easy to decouple it from the Service:

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

Now, unit testing our Service through an in-memory PostRepository implementation is pretty easy:

namespace ApplicationService;

use DomainModelBody;
use DomainModelPost;
use DomainModelPostId;
use InfrastructurePersistenceInMemoryInMemoryPostRepositor;

class LatestPostsFeedServiceTest extends PHPUnit_Framework_TestCase
{
/**
* @var InfrastructurePersistenceInMemoryInMemoryPostRepository
*/
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']);
}
}
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset