Dec 15, 2014
Table of contents:
When a web application is small it is very easy to work with. Small applications have limited scope, few complications and everything you need is within reaching distance.
However, as an application begins to grow in scope and responsibility, a small-scale application structure no longer works.
Suddenly having every object within reaching distance makes things more complicated as the scope of each object starts leaking into unrelated aspects of the application.
When you feel this kind of unease its usually a sign that you have to rethink your structure to accommodate this new design and the growing requirements of the application.
Entities are one of the most important aspects of an application because they hold a lot of knowledge and power. With great power comes great responsibility, and so it is important to put the right structure in place to ensure these objects do not get out of hand.
In today’s tutorial we’re going to be looking at using Entities in different Bounded Contexts within an application.
It’s a universal truth that all applications start off life as these beautifully simple codebases. When an application is small the code is simple, the structure is easy to understand and everything you could possibly need is within reaching distance.
However as the scope of an application increases, so too does the complexity. Now having every object within reaching distance isn’t such a great thing.
If you don’t actively evolve your application’s structure during this growth phase you end up waking up one day with a total mess on your hands.
One of the most important types of object within any application is the Entities. An Entity will hold a lot of power and responsibility within the application, particularly a Domain Driven Design application.
In order to control the scope of Entity objects and to ensure that they don’t become monolithic and leak into unrelated aspects of the application, we need to put restrictions and constraints in place.
These restrictions can feel awkward in small applications because you lose the ability to simply grab the object you need whenever you need it. However, constraints are a beautiful design tool that promote simplicity and understanding, two things that you desperately need as an application continues to grow.
So far in this building Cribbb journey we’ve built out the Identity
Bounded Context and we’ve established the foundation of the Groups
Bounded Context.
Like most web application, the users are represented by the User
Entity. This Entity lives inside of the Identity
because the User Entity is responsible for the lifecycle of the user within the application. This involves registration, updating the user’s personal details and following other users. All of this functionality is core to the concept of “identity”.
A couple of weeks ago we introduced the next Bounded Context in the form of Groups
. Cribbb has the concept of groups that act as a bucket for users to gather around to talk about certain topics and to share relevant content.
A group should have many Members and a couple of Admins. A Member should be able to contribute to the Group, and an Admin should have extra abilities to curate the content or delete spam.
As the development of this application progresses we will likely introduce even more “roles” that a User could take on. For example, in the context of a Discussion, Users might have different roles depending on who created the discussion verses the people that have contributed or simply lurked.
So as you can see, the idea of Users as different roles in the application is quickly getting more complex.
In the Identity
Bounded Context we have the singular definition of a User and a limited set of functionality based upon the concept of identity.
In the Groups
Bounded Context we have the idea of being a Member or an Admin, and the different sets of responsibilities those two roles would have access to.
And potentially we have even more granular breakdowns of roles that we need to model in a further separate Bounded Context of Discussion
.
Each of these roles are specific to the Bounded Context in which they lie. Trying to model the granularity of the these different roles and responsibilities within the Identity
namespace is quickly going to get out of hand.
Instead we need to concentrate the modelling of this functionality within the relevant Bounded Context. Instead of adding complexity, we can reduce it by adding constraints.
Last week we looked at building out the Group
Entity and the Domain Objects of the Groups
Bounded Context.
An important part of the business logic around Groups is that a User can be a Member or an Admin. This means the Group
Entity should have two Many-to-Many relationships with the User
Entity to mark the difference in responsibility.
As we covered in Creating the Twitter Follower Model using Doctrine, an important concept when defining Doctrine relationships is the Owning side and the Inverse side.
Typically it makes sense to have the Owning side within the Bounded Context that is really focused on this particular piece of functionality.
However in this instance I’m going to make the User
Entity the Owning side, rather than the Group
Entity.
This is because as a User, you join a group, rather than being added to a Group. I therefore think it makes more sense to have the User Entity as the Owning side of the relationship. I think it’s important to write your code in a way that makes sense in the context of the business, rather than the context of code.
So the first thing to do is to add the relationships to the Group
and User
Entities.
In the Group
Entity we can add the following properties and annotations:
/**
* @ORM\ManyToMany(targetEntity="Cribbb\Domain\Model\Identity\User", mappedBy="adminOf")
**/
private $admins;
/**
* @ORM\ManyToMany(targetEntity="Cribbb\Domain\Model\Identity\User", mappedBy="memberOf")
**/
private $members;
And in the User
Entity we can add the following two properties and annotations:
/**
* @ORM\ManyToMany(targetEntity="Cribbb\Domain\Model\Groups\Group", inversedBy="admins")
* @ORM\JoinTable(name="admins_groups")
**/
private $adminOf;
/**
* @ORM\ManyToMany(targetEntity="Cribbb\Domain\Model\Groups\Group", inversedBy="members")
* @ORM\JoinTable(name="members_groups")
**/
private $memberOf;
Next we can add the following methods for adding a User to a Group and to return the Collection of Entities in the relationships.
In the Group
Entity we have the following methods:
/**
* Add a User to the Group as a Member
*
* @param User $user
* @return void
*/
public function addMember(User $user)
{
$this->members[] = $user;
}
/**
* Return the Members of the Group
*
* @return ArrayCollection
*/
public function members()
{
return $this->members;
}
/**
* Add an User to the Group as an Admin
*
* @param User $user
* @return void
*/
public function addAdmin(User $user)
{
$this->admins[] = $user;
}
/**
* Return the Admins of the Group
*
* @return ArrayCollection
*/
public function admins()
{
return $this->admins;
}
And in the User
Entity we have the following methods:
/**
* Add the User as a Member of a Group
*
* @param Group $group
* @return void
*/
public function addAsMemberOf(Group $group)
{
$this->memberOf[] = $group;
$group->addMember($this);
}
/**
* Return the Groups the User is a member of
*
* @return ArrayCollection
*/
public function memberOf()
{
return $this->memberOf;
}
/**
* Return the Groups the User is an admin of
*
* @return ArrayCollection
*/
public function adminOf()
{
return $this->adminOf;
}
/**
* Add the User as an Admin of a Group
*
* @param Group $group
* @return void
*/
public function addAsAdminOf(Group $group)
{
$this->adminOf[] = $group;
$group->addAdmin($this);
}
In order to test this functionality we can add the following two tests. I’m adding these tests to the UserTest
file:
/** @test */
public function should_become_a_member_of_a_group()
{
$user = User::register(
new UserId(Uuid::uuid4()),
new Email('zuck@facebook.com'),
new Username('zuck'),
new HashedPassword('facemash')
);
$group = new Group(
new GroupId(Uuid::uuid4()),
new Name('Porcellian'),
new Slug('porcellian')
);
$this->assertEquals(0, $group->members()->count());
$this->assertEquals(0, $user->memberOf()->count());
$user->addAsMemberOf($group);
$this->assertEquals(1, $group->members()->count());
$this->assertEquals(1, $user->memberOf()->count());
}
/** @test */
public function should_become_an_admin_of_a_group()
{
$user = User::register(
new UserId(Uuid::uuid4()),
new Email('zuck@facebook.com'),
new Username('zuck'),
new HashedPassword('facemash')
);
$group = new Group(
new GroupId(Uuid::uuid4()),
new Name('Porcellian'),
new Slug('porcellian')
);
$this->assertEquals(0, $group->admins()->count());
$this->assertEquals(0, $user->adminOf()->count());
$user->addAsAdminOf($group);
$this->assertEquals(1, $group->admins()->count());
$this->assertEquals(1, $user->adminOf()->count());
}
In both of these tests we simply create a new User
and a new Group
and then we add the user as either a member or an admin and assert that the count of the Collections are correct.
Now that we have created the relationships between the Group
Entity and the User
Entity we can start to think more deeply about how this functionality should be modelled.
The User
Entity is an integral Entity within the context of Cribbb as a whole, but it is only really important within the Bounded Context of Identity
. We don’t want to expose functionality that is limited to the Identity
Bounded Context because this would leak responsibility outside of that particular boundary.
We also need to show the difference between Members and Admins. Under the hood, both Members and Admins are Users, but within the Groups
Bounded Context, they don’t require the lifecycle of an Entity.
Instead, Members and Admins should be Value Objects. Working with Value Objects is much easier than working with Entities because you don’t need to be concerned with the lifecycle of the object.
So in order to model the Members and Admins within the Groups
Bounded Context, we need to create two new Value Objects.
First we have the Member
Value Object:
<?php namespace Cribbb\Domain\Model\Groups;
use Cribbb\Domain\ValueObject;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Username;
class Member implements ValueObject
{
/**
* @var UserId
*/
private $id;
/**
* @var Email
*/
private $email;
/**
* @var Username
*/
private $username;
/**
* Create a new Member
*
* @param UserId $id
* @param Email $email
* @param Username $username
* @return void
*/
public function __construct(UserId $id, Email $email, Username $username)
{
$this->id = $id;
$this->email = $email;
$this->username = $username;
}
/**
* Determine equality with another Value Object
*
* @param ValueObject $object
* @return bool
*/
public function equals(ValueObject $object)
{
return $this == $object;
}
}
And secondly we have the Admin
Value Object:
<?php namespace Cribbb\Domain\Model\Groups;
use Cribbb\Domain\ValueObject;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Username;
class Admin implements ValueObject
{
/**
* @var UserId
*/
private $id;
/**
* @var Email
*/
private $email;
/**
* @var Username
*/
private $username;
/**
* Create a new Admin
*
* @param UserId $id
* @param Email $email
* @param Username $username
* @return void
*/
public function __construct(UserId $id, Email $email, Username $username)
{
$this->id = $id;
$this->email = $email;
$this->username = $username;
}
/**
* Determine equality with another Value Object
*
* @param ValueObject $object
* @return bool
*/
public function equals(ValueObject $object)
{
return $this == $object;
}
}
You will notice that I’m passing in the UserId
as a dependency of both objects. In this context the UserId
is simply a property of the Value Object, rather than linked to the lifecycle of the Entity.
The tests for both of these Value Objects are pretty simple and fairly similar to the other Value Objects of the application.
The Member
tests looks like this:
<?php namespace Cribbb\Tests\Domain\Model\Groups;
use Cribbb\Domain\Model\Groups\Member;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Username;
class MemberTest extends \PHPUnit_Framework_TestCase
{
/** @var UserId */
private $id;
/** @var Email */
private $email;
/** @var Username */
private $username;
/** @var Member */
private $member;
public function setUp()
{
$this->id = UserId::generate();
$this->email = new Email("name@domain.com");
$this->username = new Username("username");
$this->member = new Member($this->id, $this->email, $this->username);
}
/** @test */
public function should_require_user_id()
{
$this->setExpectedException("Exception");
$member = new Member(null, $this->email, $this->username);
}
/** @test */
public function should_require_email()
{
$this->setExpectedException("Exception");
$member = new Member($this->id, null, $this->username);
}
/** @test */
public function should_require_username()
{
$this->setExpectedException("Exception");
$member = new Member($this->id, $this->email, null);
}
/** @test */
public function should_test_equality()
{
$one = new Member($this->id, $this->email, $this->username);
$two = new Member($this->id, $this->email, $this->username);
$three = new Member(
UserId::generate(),
new Email("other@domain.com"),
new Username("other")
);
$this->assertTrue($one->equals($two));
$this->assertFalse($one->equals($three));
}
}
And the Admin
tests look like this:
<?php namespace Cribbb\Tests\Domain\Model\Groups;
use Cribbb\Domain\Model\Groups\Admin;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Username;
class AdminTest extends \PHPUnit_Framework_TestCase
{
/** @var UserId */
private $id;
/** @var Email */
private $email;
/** @var Username */
private $username;
/** @var Admin */
private $admin;
public function setUp()
{
$this->id = UserId::generate();
$this->email = new Email("name@domain.com");
$this->username = new Username("username");
$this->admin = new Admin($this->id, $this->email, $this->username);
}
/** @test */
public function should_require_user_id()
{
$this->setExpectedException("Exception");
$admin = new Admin(null, $this->email, $this->username);
}
/** @test */
public function should_require_email()
{
$this->setExpectedException("Exception");
$admin = new Admin($this->id, null, $this->username);
}
/** @test */
public function should_require_username()
{
$this->setExpectedException("Exception");
$admin = new Admin($this->id, $this->email, null);
}
/** @test */
public function should_test_equality()
{
$one = new Admin($this->id, $this->email, $this->username);
$two = new Admin($this->id, $this->email, $this->username);
$three = new Admin(
UserId::generate(),
new Email("other@domain.com"),
new Username("other")
);
$this->assertTrue($one->equals($two));
$this->assertFalse($one->equals($three));
}
}
We now face the problem of translating between Users and instances of Member
or Admin
. When we access the Collection of Members or Admins of a particular Group, we don’t want to expose the User
Entity.
Moving objects between Bounded Contexts will often require some sort of translation process. This is important because it allows you to restrict functionality and it prevents one model leaking into another. You can read more on this subject in my previous article Strategies for Integrating Bounded Contexts
To solve this problem we need a way of automatically translating the User
Entity objects to Value Objects when we retrieve the relationship from the Group
Entity.
However, it’s not the Group
Entity’s responsibility to translate objects back and forth. Instead we need to create a separate service to handle this process.
Within the Services
namespace I’m going to create a new namespace for Groups
and add a new class for UserInGroupTranslator
. The code for this class looks like this:
<?php namespace Cribbb\Domain\Services\Groups;
use Cribbb\Domain\Model\Groups\Admin;
use Cribbb\Domain\Model\Groups\Member;
use Cribbb\Domain\Model\Identity\User;
class UserInGroupTranslator
{
/**
* Translate a User to a Member
*
* @param User $user
* @return Member
*/
public function memberFrom(User $user)
{
return new Member($user->id(), $user->email(), $user->username());
}
/**
* Translate a User to an Admin
*
* @param User $user
* @return Admin
*/
public function adminFrom(User $user)
{
return new Admin($user->id(), $user->email(), $user->username());
}
}
As you can see, the code for this class is fairly simple. We simply need to accept an instance of User
and return an instance of either Member
or Admin
.
The tests for this class are equally as simple because all we need to do is to provide a User
object and assert that we are returned either a Member
object or an Admin
object:
<?php namespace Cribbb\Tests\Domain\Services\Groups;
use Cribbb\Domain\Model\Identity\User;
use Cribbb\Domain\Model\Identity\Email;
use Cribbb\Domain\Model\Identity\UserId;
use Cribbb\Domain\Model\Identity\Username;
use Cribbb\Domain\Model\Identity\HashedPassword;
use Cribbb\Domain\Services\Groups\UserInGroupTranslator;
class UserInGroupTranslatorTest extends \PHPUnit_Framework_TestCase
{
/** @var User */
private $user;
/** @var UserInGroupTranslator */
private $translator;
public function setUp()
{
$this->user = User::register(
UserId::generate(),
new Email("name@domain.com"),
new Username("username"),
new HashedPassword("password")
);
$this->translator = new UserInGroupTranslator();
}
/** @test */
public function should_create_member_from_user()
{
$member = $this->translator->memberFrom($this->user);
$this->assertInstanceOf("Cribbb\Domain\Model\Groups\Member", $member);
}
/** @test */
public function should_create_admin_from_user()
{
$admin = $this->translator->adminFrom($this->user);
$this->assertInstanceOf("Cribbb\Domain\Model\Groups\Admin", $admin);
}
}
With the UserInGroupTranslator
class created we can now add it to the Group
Entity.
Within the __construct()
method of the Group
Entity, instantiate a new instance of the UserInGroupTranslator
:
/**
* Create a new Group
*
* @param GroupId $groupId
* @param Name $name
* @param Slug $slug
* @return void
*/
public function __construct(GroupId $groupId, Name $name, Slug $slug)
{
$this->setId($groupId);
$this->setName($name);
$this->setSlug($slug);
$this->admins = new ArrayCollection;
$this->members = new ArrayCollection;
$this->userInGroupTranslator = new UserInGroupTranslator;
}
Next update the members()
and admins()
methods to use the translator:
/**
* Return the Members of the Group
*
* @return ArrayCollection
*/
public function members()
{
return $this->members->map(function ($user) {
return $this->userInGroupTranslator->memberFrom($user);
});
}
/**
* Return the Admins of the Group
*
* @return ArrayCollection
*/
public function admins()
{
return $this->admins->map(function ($user) {
return $this->userInGroupTranslator->adminFrom($user);
});
}
In both of these methods we use the map()
method of the ArrayCollection
object to iterate over each object in the Collection and return a new Value Object instance from the translator.
The map()
method will return a new ArrayCollection
instance and so the outside world is unaware that the returned objects went through a translation process.
To test this code we can write the following tests that assert an instance of the correct type is returned from each Collection:
/** @test */
public function should_have_members_collection()
{
$group = new Group($this->id, $this->name, $this->slug);
$group->addMember($this->user);
$this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $group->members());
$this->assertInstanceOf('Cribbb\Domain\Model\Groups\Member', $group->members()->first());
}
/** @test */
public function should_have_admins_collection()
{
$group = new Group($this->id, $this->name, $this->slug);
$group->addAdmin($this->user);
$this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $group->admins());
$this->assertInstanceOf('Cribbb\Domain\Model\Groups\Admin', $group->admins()->first());
}
The majority of all applications will become more complicated as they grow. Code is usually really easy to understand, but it can get out of control if an appropriate structure is not put into place.
Entities are the most powerful objects within an application because they model the lifecycle of important concepts of the domain. Entities will usually have an important role within a particular Bounded Context of the application.
However, it’s important to not allow the power of an Entity to leak out into other Bounded Contexts. You shouldn’t be able to call potentially dangerous methods on an Entity from an unrelated part of the application.
It’s inevitable that certain Entities will have a role in multiple Bounded Contexts. As I’ve shown in this tutorial, Users have certain abilities in the Identity
Bounded Context, but we require a totally different division of responsibility in the Groups
Bounded Context.
Instead of creating a monolithic User
Entity, we can constrain the scope of the object to a single Bounded Context, and model the extraneous functionality as Value Objects.
Now of course, you probably won’t need this for every application you work on. But, adding constraints and restricting the scope of your design can create a lot more clarity and simplicity when working with larger applications.
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.