Dec 29, 2014
Table of contents:
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.
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.
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.
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.
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.
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.
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()
);
}
}
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.
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.