No Invariant, Two Aggregates

We'll discuss Application Services in the following chapters, but for now, let's check different approaches for making a Wish. The first approach, particularly for a novice, would likely be something similar to this:

class MakeWishService
{
private $wishRepository;

public function __construct(WishRepository $wishRepository)
{
$this->wishRepository = $wishRepository;
}

public function execute(MakeWishRequest $request)
{
$userId = $request->userId();
$address = $request->address();
$content = $request->content();

$wish = new Wish(
$this->wishRepository->nextIdentity(),
new UserId($userId),
$address,
$content
);

$this->wishRepository->add($wish);
}
}

This code probably allows for the best performance possible. You can almost see the INSERT statement behind the scenes; the minimum number of operations for such a use case is one, which is good. With the current implementation, we can create as many wishes as we want, according to the business requirements, which is also good.

However, there may be a potential issue: we can create wishes for a user who may not exist in the Domain. This is a problem, regardless of what technology we're using for persisting Aggregates. Even if we're using an in-memory implementation, we can create a Wish without its corresponding User.

This is broken business logic. Of course, this can be fixed using a foreign key in the database, from wish (user_id) to user(id), but what happens if we're not using a database with foreign keys? And what happens if it's a NoSQL database, such as Redis or Elasticsearch?

If we want to fix this issue so that the same code can work properly in different infrastructures, we need to check if the user exists. It's likely that the easiest approach is in the same Application Service:

class MakeWishService
{
// ...
public function execute(MakeWishRequest $request)
{
$userId = $request->userId();
$address = $request->address();
$content = $request->content();

$user = $this->userRepository->ofId(new UserId($userId));
if (null === $user) {
throw new UserDoesNotExistException();
}

$wish = new Wish(
$this->wishRepository->nextIdentity(),
$user->id(),
$address,
$content
);

$this->wishRepository->add($wish);
}
}

That could work, but there's a problem performing the check in the Application Service: this check is high in the delegation chain. If any other code snippet that isn't this Application Service — such as a Domain Service or another Entity — wants to create a Wish for a non-existing User, it can do it. Take a look at the following code:

// Somewhere in a Domain Service or Entity
$nonExistingUserId = new UserId('non-existing-user-id');
$wish = new Wish(
$this->wishRepository->nextIdentity(),
$nonExistingUserId,
$address,
$content
);

If you've already read Chapter 9, Factories, then you have the solution. Factories help us keep the business invariants, and that's exactly what we need here.

There's an implicit invariant saying that we're not allowed to make a wish without a valid user. Let's see how a factory can help us:

abstract class WishService
{
protected $userRepository;
protected $wishRepository;

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

protected function findUserOrFail($userId)
{
$user = $this->userRepository->ofId(new UserId($userId));
if (null === $user) {
throw new UserDoesNotExistException();
}

return $user;
}

protected function findWishOrFail($wishId)
{
$wish = $this->wishRepository->ofId(new WishId($wishId));
if (!$wish) {
throw new WishDoesNotExistException();
}

return $wish;
}

protected function checkIfUserOwnsWish(User $user, Wish $wish)
{
if (!$wish->userId()->equals($user->id())) {
throw new InvalidArgumentException(
'User is not authorized to update this wish'
);
}
}
}

class MakeWishService extends WishService
{
public function execute(MakeWishRequest $request)
{
$userId = $request->userId();
$address = $request->address();
$content = $request->content();

$user = $this->findUserOrFail($userId);

$wish = $user->makeWish(
$this->wishRepository->nextIdentity(),
$address,
$content
);

$this->wishRepository->add($wish);
}
}

As you can see, Users make Wishes, and so does our code. makeWish is a Factory Method for building Wishes. The method returns a new Wish built with the UserId from the owner:

class User
{
// ...

/**
* @return Wish
*/
public function makeWish(WishId $wishId, $address, $content)
{
return new Wish(
$wishId,
$this->id(),
$address,
$content
);
}

// ...
}

Why are we returning a Wish and not just adding the new Wish to an internal collection as we would do with Doctrine? To summarize, in this scenario, User and Wish don't conform to an Aggregate because there's no true business invariant to protect. A User can add and remove as many Wishes as they want. Wishes and their User can be updated independently in the database in different transactions, if needed.

Following the rules about Aggregate Design explained before, we should aim for small Aggregates, and that's the result here. Each Entity has its own Repository. Wish references its owning User using Identities — UserId in this case. Getting all the wishes of a user can be performed by a finder in the WishRepository and paginated easily without any performance issues:

interface WishRepository
{
/**
* @param WishId $wishId
*
* @return Wish
*/
public function ofId(WishId $wishId);

/**
* @param UserId $userId
*
* @return Wish[]
*/
public function ofUserId(UserId $userId);

/**
* @param Wish $wish
*/
public function add(Wish $wish);

/**
* @param Wish $wish
*/
public function remove(Wish $wish);

/**
* @return WishId
*/
public function nextIdentity();
}

An interesting aspect of this approach is that we don't have to map the relation between User and Wish in our favorite ORM. Because we reference the User from the Wish using the UserId, we just need the Repositories. Let's consider how the mapping of such Entities using Doctrine might appear:

LwDomainModelUserUser:
type: entity
id:
userId:
column: id
type: UserId
table: user
repositoryClass:
LwInfrastructureDomainModelUserDoctrineUserRepository
fields:
email:
type: string
password:
type: string
LwDomainModelWishWish:
type: entity
table: wish
repositoryClass:
LwInfrastructureDomainModelWishDoctrineWishRepository
id:
wishId:
column: id
type: WishId
fields:
address:
type: string
content:
type: text
userId:
type: UserId
column: user_id

No relation is defined. After making a new wish, let's write some code for updating an existing one:

class UpdateWishService extends WishService
{
public function execute(UpdateWishRequest $request)
{
$userId = $request->userId();
$wishId = $request->wishId();
$email = $request->email();
$content = $request->content();

$user = $this->findUserOrFail($userId);
$wish = $this->findWishOrFail($wishId);
$this->checkIfUserOwnsWish($user, $wish);

$wish->changeContent($content);
$wish->changeAddress($email);
}
}

Because User and Wish don't form an Aggregate, in order to update the Wish, we need first to retrieve it using the WishRepository. Some extra checks ensure that only the owner can update the wish. As you may have seen, $wish is already an existing Entity in our Domain, so there's no need to add it back again using the Repository. However, in order to make changes durable, our ORM must be aware of the information updated and flush any remaining changes to the database after the work is done. Don't worry; we'll take a look closer at this in Chapter 11, Application. In order to complete the example, let's take a look at how to remove a wish:

class RemoveWishService extends WishService
{
public function execute(RemoveWishRequest $request)
{
$userId = $request->userId();
$wishId = $request->wishId();

$user = $this->findUserOrFail($userId);
$wish = $this->findWishOrFail($wishId);
$this->checkIfUserOwnsWish($user, $wish);

$this->wishRepository->remove($wish);
}
}

As you may have seen, you could refactor some parts of the code, such as the constructor and the ownership checks, to reuse them in both Application Services. Feel free to consider how you would do that. Last but not least, how could we get all the wishes of a specific user:

class ViewWishesService extends WishService
{
/**
* @return Wish[]
*/
public function execute(ViewWishesRequest $request)
{
$userId = $request->userId();
$wishId = $request->wishId();

$user = $this->findUserOrFail($userId);
$wish = $this->findWishOrFail($wishId);
$this->checkIfUserOwnsWish($user, $wish);

return $this->wishRepository->ofUserId($user->id());
}
}

This is quite straightforward. However, we'll go deeper into how to render and return information from Application Services in the corresponding chapter. For now, returning a collection of Wishes will do the job.
Let's sum up this non-Aggregate approach. We couldn't find any true business invariant to consider User and Wish as an Aggregate, which is why each of them is an Aggregate. User has its own Repository, UserRepository. Wish has its own Repository too, WishRepository. Each Wish holds a UserId reference to owner, User. Even so, we didn't require a transaction. That's the best scenario in terms of performance and scalability issues. However, life is not always so wonderful. Consider what could happen with a true business invariant.

..................Content has been hidden....................

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