cult3

Enforcing Business Rules through Aggregate Instantiation

Dec 29, 2014

Table of contents:

  1. A recap of the business rules
  2. Using an Aggregate to create another Aggregate
  3. What are the benefits of this approach?
  4. The Thread Entity
  5. Creating a new Thread
  6. Conclusion

As we looked at in What are Aggregates in Domain Driven Design?, Aggregates group related Entities into a single unit to provide a constrained and unified interface through a root Entity. This makes working with those Entities easier because you don’t need to worry about the Aggregate’s internal details.

Domain Driven Design stands on the shoulders of Object Oriented Programming. As we’ve seen a couple of times in this series, a good Object Oriented approach for ensuring that an object has certain attributes is to require them on instantiation.

Therefore, we can use the native characteristics of the PHP programming language to ensure that objects are instantiated with everything they need.

The instantiation of an object is an important part of its lifecycle and provides us with an opportunity to implement business logic and enforce the rules of the application.

In today’s tutorial we’re going to be looking at how we can use the moment of instantiation to our advantage.

A recap of the business rules

Last week we looked at Modelling a Discussion Forum and the application rules we will need to enforce through code.

If you haven’t read last week’s post I would urge you to do that first. Here is a recap of what was decided.

A Thread must belong to a Group

The Thread Entity will be the Aggregate root of the Discussion Bounded Context, but will belong to a Group from the Groups Bounded Context.

Discussions will be grouped together into specific topics in much the same way as Reddit has subreddits.

A User must be a Member of a Group to create a Thread

In order to create Threads in Cribbb, you need to be a Member of the Group in which it belongs.

This is taking elements from three different Bounded Contexts. The User Entity is from the Identity Bounded Context and controls the current authenticated user. The Group Entity is from the Groups Bounded Context and deals with permissions. And the Thread Entity from the Discussion Bounded Context deals with the actual user’s post.

Using an Aggregate to create another Aggregate

So we face the situation that creating a new Thread is very much dependent on an existing Group. Not only does the Thread need to belong to a Group, but the User who is creating the Thread needs to be a member of the Group.

Clearly the Group Entity is critically important to the instantiation process of new Thread Entities!

When an Aggregate has this much control over another Aggregate, it makes sense to model the instantiation process as a method on the existing object.

In this case, we can create a method on the Group Aggregate that will create a new Thread Aggregate.

This method will require the authenticated User object to check that the user is a member of the Group.

The method will then return a fully formed Thread object that belongs to the Group it was created from.

What are the benefits of this approach?

Before I jump into the code, first I think it’s important that we look at the benefits of this approach.

The value of Object Oriented Programming is the encapsulation of logic inside of the object. The consumer of an object should never be concerned with how the object is actually implemented.

An object should only ever be concerned with the logic that it encapsulates.

Often, as in this case, the instantiation of an object is a complicated process. When an object tries to also encapsulate a complicated instantiation process, it starts to lose sight of it’s single responsibility.

As we looked at in What are Factories in Domain Driven Design?, an object does not necessary need to be concerned with it’s own instantiation process. A car engine is a very intricate machine, but we don’t require the engine to self-assemble.

Instead we can move this responsibility to a dedicated object or method. In this case we are moving that responsibility to a method on the Group Aggregate.

The benefits of this approach are:

Encapsulate the business rules By moving the responsibility for instantiating new Thread Aggregates to the Group Aggregate we can very easily check to ensure that the User is a member and that the new Thread belongs to the correct Group.

Stay close to the Ubiquitous Language The Ubiquitous Language states that a Thread must belong to a Group. By giving responsibility to the Group Aggregate we are staying close to the Ubiquitous Language.

Avoid the Anaemic Model problem Theoretically we could also implement this logic in a Service class, however that would be stealing responsibility from our Domain Objects. You should always aim to implement logic on Domain Objects, and only use Services as a last resort. You can read more about the Anaemic Model problem in Creating Domain Services.

The Thread Entity

So the first thing we will do will be to create the Thread Entity.

First we need to create the ThreadId object:

<?php namespace Cribbb\Domain\Model\Discussion;

use Rhumsaa\Uuid\Uuid;
use Cribbb\Domain\Identifier;
use Cribbb\Domain\UuidIdentifier;

class ThreadId extends UuidIdentifier implements Identifier
{
    /**
     * @var Uuid
     */
    protected $value;

    /**
     * Create a new ThreadId
     *
     * @return void
     */
    public function __construct(Uuid $value)
    {
        $this->value = $value;
    }
}

And the ThreadIdTest file:

<?php namespace Cribbb\Tests\Domain\Model\Discussion;

use Rhumsaa\Uuid\Uuid;
use Cribbb\Domain\Model\Discussion\ThreadId;

class ThreadIdTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_require_instance_of_uuid()
    {
        $this->setExpectedException("Exception");

        $id = new ThreadId();
    }

    /** @test */
    public function should_create_new_thread_id()
    {
        $id = new ThreadId(Uuid::uuid4());

        $this->assertInstanceOf("Cribbb\Domain\Model\Discussion\ThreadId", $id);
    }

    /** @test */
    public function should_generate_new_thread_id()
    {
        $id = ThreadId::generate();

        $this->assertInstanceOf("Cribbb\Domain\Model\Discussion\ThreadId", $id);
    }

    /** @test */
    public function should_create_thread_id_from_string()
    {
        $id = ThreadId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");

        $this->assertInstanceOf("Cribbb\Domain\Model\Discussion\ThreadId", $id);
    }

    /** @test */
    public function should_test_equality()
    {
        $one = ThreadId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
        $two = ThreadId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");
        $three = ThreadId::generate();

        $this->assertTrue($one->equals($two));
        $this->assertFalse($one->equals($three));
    }

    /** @test */
    public function should_return_thread_id_as_string()
    {
        $id = ThreadId::fromString("d16f9fe7-e947-460e-99f6-2d64d65f46bc");

        $this->assertEquals(
            "d16f9fe7-e947-460e-99f6-2d64d65f46bc",
            $id->toString()
        );
        $this->assertEquals(
            "d16f9fe7-e947-460e-99f6-2d64d65f46bc",
            (string) $id
        );
    }
}

If you have been following along with this series this code will be very familiar to you.

Next we can create the Thread Entity:

<?php namespace Cribbb\Domain\Model\Discussion;

use Cribbb\Domain\RecordsEvents;
use Doctrine\ORM\Mapping as ORM;
use Cribbb\Domain\AggregateRoot;

/**
 * @ORM\Entity
 * @ORM\Table(name="threads")
 */
class Thread implements AggregateRoot
{
    use RecordsEvents;
}

As we’ve seen in previous tutorials, this is a Doctrine Entity (Working with Entities in Doctrine 2) and so it requires the annotations.

We also need to mark this Entity as an AggregateRoot and we can include the RecordsEvents trait to implement Domain Events (Implementing Domain Events).

Next we can include the Entity’s attribute declarations:

/**
 * @ORM\Id
 * @ORM\Column(type="string")
 */
private $id;

/**
 * @ORM\Column(type="string")
 */
private $subject;

/**
 * @ORM\Column(type="string")
 */
private $slug;

The Entity will require a unique id, a subject and a slug. The slug is automatically generated from the subject to allow threads to be found via URL.

We also need to implement the relationship with the Group Entity:

/**
 * @ORM\ManyToOne(targetEntity="Cribbb\Domain\Model\Groups\Group", inversedBy="threads")
 **/
private $group;

This is a standard Many To One relationship and so we only require this simple annotation to inform Doctrine.

Next we can write the __construct() method:

/**
 * Create a new Thread
 *
 * @param ThreadId $threadId
 * @param Group $group
 * @param string $subject
 * @return void
 */
public function __construct(ThreadId $threadId, Group $group, $subject)
{
    Assertion::string($subject);

    $this->setId($threadId);
    $this->setGroup($group);
    $this->setSubject($subject);
    $this->setSlug(Str::slug($subject));
}

First I pass in the ThreadId and Group as dependencies so we can ensure that a Thread requires these two objects on instantiation.

Next I pass in the $subject as a native string. In Creating Domain Objects Recap I used Value Objects for the Group Name and Slug, however I decided that was overcomplicating things. In this Entity I’m simply using a string for the $subject and automatically converting it to a slug internally using the Str::slug() class from Laravel.

Finally we have the standard getters and setters:

/**
 * Get the id
 *
 * @return ThreadId
 */
public function id()
{
    return ThreadId::fromString($this->id);
}

/**
 * Set the id
 *
 * @param GroupId $id
 * @return void
 */
private function setId(ThreadId $id)
{
    $this->id = $id->toString();
}

/**
 * Get the subject
 *
 * @return string
 */
public function subject()
{
    return $this->subject;
}

/**
 * Set the subject
 *
 * @param string $subject
 * @return void
 */
private function setSubject($subject)
{
    $this->subject = $subject;
}

/**
 * Get the slug
 *
 * @return string
 */
public function slug()
{
    return $this->slug;
}

/**
 * Set the slug
 *
 * @param string $slug
 * @return void
 */
private function setSlug($slug)
{
    $this->slug = $slug;
}

/**
 * Set the Group
 *
 * @return void
 */
private function setGroup(Group $group)
{
    $this->group = $group;
}

/**
 * Get the Group
 *
 * @return Group
 */
public function group()
{
    return $this->group;
}

The only thing to notice here is I’ve not prefixed the getters and I’ve got all the setters as private. I think having non-prefixed getters provides a cleaner API, and having private setters ensures you control how the object can be used.

We also need to implement the other side of the Thread / Group relationship in the Group Entity.

First we define the relationship in the class property annotation:

/**
 * @ORM\OneToMany(targetEntity="Cribbb\Domain\Model\Discussion\Thread", mappedBy="group")
 **/
private $threads;

Next we instantiate a new instance of ArrayCollection in the Group Entity __construct() method:

$this->threads = new ArrayCollection();

The tests for the Thread Entity are fairly similar to the tests for the previous Entities in this series:

<?php namespace Cribbb\Tests\Domain\Model\Discussion;

use Cribbb\Domain\Model\Groups\Group;
use Cribbb\Domain\Model\Groups\GroupId;
use Cribbb\Domain\Model\Discussion\Thread;
use Cribbb\Domain\Model\Discussion\ThreadId;

class ThreadTest extends \PHPUnit_Framework_TestCase
{
    /** @var Group */
    private $group;

    public function setUp()
    {
        $this->group = new Group(GroupId::generate(), "Cribbb");
    }

    /** @test */
    public function should_require_id()
    {
        $this->setExpectedException("Exception");

        $thread = new Thread(null, $this->group, "Hello World");
    }

    /** @test */
    public function should_require_group()
    {
        $this->setExpectedException("Exception");

        $thread = new Thread(ThreadId::generate(), null, "Hello World");
    }

    /** @test */
    public function should_require_subject()
    {
        $this->setExpectedException("Exception");

        $thread = new Thread(ThreadId::generate(), $this->group);
    }

    /** @test */
    public function should_create_thread()
    {
        $thread = new Thread(ThreadId::generate(), $this->group, "Hello World");

        $this->assertInstanceOf(
            "Cribbb\Domain\Model\Discussion\Thread",
            $thread
        );
        $this->assertInstanceOf(
            "Cribbb\Domain\Model\Discussion\ThreadId",
            $thread->id()
        );
        $this->assertEquals("Hello World", $thread->subject());
        $this->assertEquals("hello-world", $thread->slug());
        $this->assertInstanceOf(
            "Cribbb\Domain\Model\Groups\Group",
            $thread->group()
        );
    }
}

Creating a new Thread

Now that we have the Thread Entity defined, we can write the method on the Group Entity that should be responsible for creating new threads.

/**
 * Start a new Thread
 *
 * @param User $user
 * @param string $subject
 * @return Thread
 */
public function startNewThread(User $user, $subject)
{
    if ($this->members->contains($user)) {
        $thread = new Thread(ThreadId::generate(), $this, $subject);

        $this->addThread($thread);

        return $thread;
    }

    throw new Exception('This user is not a member of the Group!');
}

This method requires an instance of User (the currently authenticated user) and the $subject of the new thread.

First we check to ensure the User is a member of the Group:

if ($this->members->contains($user)) {
}

throw new Exception("This user is not a member of the Group!");

If the user is not a member we can just abort the process by throwing a new Exception.

If the user is a member we can instantiate a new Thread, assign it to the Group and return it from the method:

$thread = new Thread(ThreadId::generate(), $this, $subject);

$this->threads[] = $thread;

return $thread;

I’ve also defined a public method for returning the collection of Thread objects:

/**
 * Return the Threads Collection
 *
 * @return ArrayCollection
 */
public function threads()
{
    return $this->threads;
}

To test this functionality we can add the following two tests to the GroupTest file.

First we can check to ensure that a new Thread is created correctly:

/** @test */
public function should_create_a_new_thread()
{
    $group = new Group(GroupId::generate(), 'Cribbb');
    $group->addMember($this->user);
    $thread = $group->startNewThread($this->user, 'Hello World');

    $this->assertInstanceOf('Cribbb\Domain\Model\Discussion\Thread', $thread);
    $this->assertEquals(1, $group->threads()->count());
}

In this test I’m basically just going through the motions of creating a new Thread and then asserting that the number of threads is correct.

Next we can assert that only members of the Group can create new Threads:

/** @test */
public function should_throw_exception_when_non_member_attempts_to_create_thread()
{
    $this->setExpectedException('Exception');

    $group = new Group(GroupId::generate(), 'Cribbb');
    $thread = $group->startNewThread($this->user, 'Hello World');
}

In this test we are asserting that an Exception is being thrown when a non-member attempts to create a Thread in a Group they don’t belong to.

Conclusion

Aggregates play an important role in Domain Driven Design projects by grouping functionality as a single unit and by providing a simple interface for working with many interrelated objects.

Domain Driven Design is very much built on the principles of Object Oriented Programming and so we can use many of the features of the PHP programming language.

One of these native features is requiring certain arguments when an object is instantiated. If we require an object to have certain dependencies, we can enforce this using native PHP code.

Objects should have a single responsibility, but it can be easy to lose site of this goal when the instantiation process of an object becomes complicated. Not all objects should be responsible for their own instantiation process.

Instead of overloading an object with complexity, instead move the instantiation process to an appropriate place. Typically this will be a stand alone Factory object or a method on an existing Domain object.

It can be tempting to just shove this kind of code into a Service and call it a day, but you will be stealing vital responsibility away from your Domain objects. Instead of falling victim to the Anaemic Model problem, you should endeavour to stay as close to the Ubiquitous Language as possible.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.