5. Domain
“A Domain [...] is what an organization
does and the world it does in.”
(Vaughn Vernon, “Implementing Domain-Driven Design”)
6. Domain Model
“A model is a simplification. It is an
interpretation of reality that abstracts the
aspects relevant to solving problem at hand
and ignores extraneous detail.”
(Eric Evans, "Domain Driven Design")
8. Domain Model
“In DDD, domain model refers to a
class.”
(Julie Lerman, http://msdn.microsoft.com/en-us/magazine/dn385704.aspx)
9. Domain Model
“An object model of the domain that
incorporates both behavior and data.”
(Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
10. Domain Model
“An object model of the domain that
incorporates both behavior and data.”
(Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
12. First, because it’s not anemic
Martin Fowler
http://martinfowler.com/bliki/AnemicDomainModel.html
13. <?php
class Anemic
{
private $shouldNotChangeAfterCreation;
private $shouldBeChangedOnlyOnEdit;
private $couldBeChangedAnytime;
public function setCouldBeChangedAnytime($couldBeChangedAnytime) {}
public function setShouldBeChangedOnlyOnEdit($shouldBeChangedOnlyOnEdit) {}
public function setShouldNotChangeAfterCreation($shouldNotChangeAfterCreation {}
}
14. <?php
class SomeService
{
public function doStuffOnCreation()
{
$anemic = new Anemic();
$anemic->setCouldBeChangedAnytime('abc');
$anemic->setShouldNotChangeAfterCreation('def');
$unitOfWork->persist($anemic);
$unitOfWork->flush();
}
}
15. <?php
class SomeService
{
public function doStuffOnEdit()
{
$anemic = new Anemic();
$anemic->setCouldBeChangedAnytime('abc');
$anemic->setShouldBeChangedOnlyOnEdit(‘123’);
$unitOfWork->persist($anemic);
$unitOfWork->flush();
}
}
16. <?php
class SomeService
{
public function doStuffOnEdit()
{
Loss of memory
$anemic = new Anemic();
$anemic->setCouldBeChangedAnytime('abc');
$anemic->setShouldBeChangedOnlyOnEdit(‘123’);
$unitOfWork->persist($anemic);
$unitOfWork->flush();
}
}
17. <?php
class SomeService
{
public function doStuffOnEdit()
{
$anemic = new Anemic();
$anemic->setCouldBeChangedAnytime('abc');
$anemic->setShouldBeChangedOnlyOnEdit(‘123’);
$anemic->setShouldNotChangeAfterCreation('def');
$unitOfWork->persist($anemic);
$unitOfWork->flush();
}
}
18. <?php
class BehaviouralClass
{
private $shouldNotChangeAfterCreation;
private $shouldBeChangedOnlyOnEdit;
private $couldBeChangedAnytime;
public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime)
{
$this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation;
$this->couldBeChangedAnytime = $couldBeChangedAnytime;
}
public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null)
{
$this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit;
$this->couldBeChangedAnytime = $couldBeChangedAnytime;
}
}
19. <?php
class BehaviouralClass
{
private $shouldNotChangeAfterCreation;
private $shouldBeChangedOnlyOnEdit;
private $couldBeChangedAnytime;
It's not still rich, lacks of ...
public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime)
{
$this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation;
$this->couldBeChangedAnytime = $couldBeChangedAnytime;
}
public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null)
{
$this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit;
$this->couldBeChangedAnytime = $couldBeChangedAnytime;
}
}
20. Ubiquitous Language
“the domain model can provide the backbone
for that common language [...]. The vocabulary
of that ubiquitius language includes the names
of classes and prominent operations”
(Eric Evans, "Domain Driven Design")
26. The meaning of “player”
Within game engine context
a model of a real soccer player,
modelled with behaviours to fit the
requirements of the game engine.
27. The meaning of “player”
Within data import context
a model of a real soccer player, but
modelled for a simple CRUD.
28. The meaning of “player”
Within user profile context
a model of the user of the website,
who plays the game.
33. <?php
class League
{
private $id;
private $name;
private $teams;
public function __construct(Uuid $uuid, $name)
{
$this->id = $uuid;
$this->name = $name;
$this->teams = new ArrayCollection();
}
public function registerTeam(Team $team)
{
$this->teams->add($team);
}
}
34. <?php
class League
{
private $id;
private $name;
private $teams;
A team must do a registration
public function __construct({
to the league
Uuid $uuid, $name)
$this->id = $uuid;
$this->name = $name;
$this->teams = new ArrayCollection();
}
public function registerTeam(Team $team)
{
$this->teams->add($team);
}
}
35. <?php
class League
{
private $id;
private $genericInfo;
private $teams;
public function __construct(Uuid $uuid,
LeagueGenericInfo $leagueGenericInfo)
{
$this->id = $uuid;
$this->genericInfo = $leagueGenericInfo;
$this->teams = new ArrayCollection();
}
// ...}
36. <?php
class LeagueGenericInfo
{
private $name;
private $description;
private $country;
public function __construct($country, $description, $name)
{
$this->country = $country;
$this->description = $description;
$this->name = $name;
}
// … getters and behaviours}
37. <?php
class LeagueGenericInfo
{
private $name;
private $description;
private $country;
Value object
public function __construct($country, $description, $name)
{
$this->country = $country;
$this->description = $description;
$this->name = $name;
}
// … getters and behaviours}
39. <?php
class League
{
// ...
public function registerTeam(Team $team)
{
if (!$this->canLeagueAcceptAnotherRegistration()) {
throw new DomainException('Not more places available');
}
$this->teams->add($team);
}
private function canLeagueAcceptAnotherRegistration()
{
if ($this->teams->count() == 8) {
return false;
}
return true;
}
}
40. <?php
class League
{
// ...
public function registerTeam(Team $team)
{
if (!$this->canLeagueAcceptAnotherRegistration()) {
League protects its invariants
throw new DomainException('Not more places available');
}
$this->teams->add($team);
}
private function canLeagueAcceptAnotherRegistration()
{
if ($this->teams->count() == 8) {
return false;
}
return true;
}
}
41. <?php
class League
{
// ...
public function getTeams()
{
return $this->teams;
}
}
42. <?php
class League
{
// ...
private function getTeams()
{
return $this->teams;
}
}
43. <?php
class LeagueGenericInfo
{
private $name;
private $description;
private $country;
private static $countries;
public function __construct($country, $description, $name)
{
if(!isset(static::$countries)) {
static::$countries = require __DIR__.'/countries.php';
}
if (!array_key_exists($name, static::$countries)) {
throw new UnknownCountryException($country);
}
$this->country = $country;
// .. thanks to Mathias Verraes for “Money” ;-)
}}
44. <?php
class LeagueGenericInfo
{
private $name;
private $description;
private $country;
Input validation
private static $countries;
public function __construct($country, $description, $name)
{
if(!isset(static::$countries)) {
static::$countries = require __DIR__.'/countries.php';
}
if (!array_key_exists($name, static::$countries)) {
throw new UnknownCountryException($country);
}
$this->country = $country;
// .. thanks to Mathias Verraes for “Money” ;-)
}}
45. <?php
class LeagueGenericInfo
{
private $name;
private $description;
private $country;
Could be private static also $countries;
placed in commands
public function __construct($country, $description, $name)
{
if(!isset(static::$countries)) {
static::$countries = require __DIR__.'/countries.php';
}
if (!array_key_exists($name, static::$countries)) {
throw new UnknownCountryException($country);
}
$this->country = $country;
// .. thanks to Mathias Verraes for “Money” ;-)
}}
46. <?php
class League
{
public function render()
{
$properties = [
'id' => $this->id,
'name' => $this->genericInfo->getName(),
'description' => $this->genericInfo->getDescription(),
'country' => $this->genericInfo->getCountry(),
];
$teams = new ArrayCollection();
foreach ($this->teams as $team) {
$teams->add($team->render());
}
$properties['teams'] = $teams;
return new ArrayCollection($properties);
}
}
47. <?php
class League
{
public function render()
{
$properties = [
'id' => $this->id,
'name' => $this->genericInfo->getName(),
'description' => $this->genericInfo->getDescription(),
'country' => $this->genericInfo->getCountry(),
];
Return a read-only object
$teams = new ArrayCollection();
foreach ($this->teams as $team) {
$teams->add($team->render());
}
$properties['teams'] = $teams;
return new ArrayCollection($properties);
}
}
48. <?php
class Team
{
private $id;
private $players;
public function __construct(Uuid $uuid)
{
$this->id = $uuid;
$this->players = new ArrayCollection();
}
public function firePlayer($id)
{
foreach ($this->players as $key => $player) {
if ($player->getId() == $id) {
$this->players->remove($key);
}
}
}
}
49. <?php
class Team
{
private $id;
private $players;
Traverse the collections
public function __construct(Uuid $uuid)
{
$this->id = $uuid;
$this->players = new ArrayCollection();
}
public function firePlayer(Player $playerToFire)
{
foreach ($this->players as $key => $player) {
if ($player->getId() == $playerToFire->getId()) {
$this->players->remove($key);
}
}
}
}
52. <?php
class LeagueTest extends PHPUnit_Framework_TestCase
{
/**
* @test
* @expectedException
*/
public function leagueMustHaveMaximumEightTeams()
{
// …
$genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league');
$league = new League($uuid, $genericInfo);
$team = $this->getMockBuilder('Team') // …
for ($x=0; $x<=8; $x++) {
$league->registerTeam($team);
}
}
}
53. <?php
class LeagueTest extends PHPUnit_Framework_TestCase
{
/**
* @test
* @expectedException
*/
public function leagueMustHaveMaximumEightTeams()
{
// …
$genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league');
$league = new League($uuid, $genericInfo);
$team = $this->getMockBuilder('Team') // …
for ($x=0; $x<=8; $x++) {
$league->registerTeam($team);
}
}
}
54. <?php
class LeagueTest extends PHPUnit_Framework_TestCase
{
/**
* @test
* @expectedException
*/
public function leagueMustHaveMaximumEightTeams()
{
// …
The same Team
can do more than one registration
to the League?!?
$genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league');
$league = new League($uuid, $genericInfo);
$team = $this->getMockBuilder('Team') // …
for ($x=0; $x<=8; $x++) {
$league->registerTeam($team);
}
}
}
55. <?php
class League
{
//..
public function registerTeam(Team $team)
{
$this->canLeagueAcceptRegistrationOf($team);
$this->teams->add($team);
}
private function canLeagueAcceptRegistrationOf(Team $applicantTeam)
{
if (!$this->canLeagueAcceptAnotherRegistration()) {
throw new DomainException('Not more places available');
}
foreach ($this->teams as $key => $team) {
if ($team->getId() == $applicantTeam->getId()) {
throw new DomainException('Team already registered');
}
}
}
}
56. <?php
class League
{
//..
public function registerTeam(Team $team)
{
$this->canLeagueAcceptRegistrationOf($team);
$this->teams->add($team);
}
And so on ...
private function canLeagueAcceptRegistrationOf(Team $applicantTeam)
{
if (!$this->canLeagueAcceptAnotherRegistration()) {
throw new DomainException('Not more places available');
}
foreach ($this->teams as $key => $team) {
if ($team->getId() == $applicantTeam->getId()) {
throw new DomainException('Team already registered');
}
}
}
}
67. Retrieve an object joined with empty collection
<?php
class TeamRepository implements TeamRepositoryInterface
{
public function getWithPlayers($id)
{
$qb = $this->em->createQueryBuilder();
$qb
->select('t', 'p')
->from("Team", 't')
->leftJoin(t.players', 'p', Join::WITH, $qb->expr()->andX(
$qb->expr()->eq('p.status', ':status')
))
->where('t.id.uuid = :id');
$qb->setParameter('status', 'on_the_market');
$qb->setParameter('id', $id);
return $qb->getQuery()->getOneOrNullResult();
}
}
68. Get paginated list of Teams with Player joined
<?php
use DoctrineORMToolsPaginationPaginator;
class TeamRepository implements TeamRepositoryInterface
{
public function paginate($first, $max)
{
$qb = $this->em->createQueryBuilder();
$qb
->select('t', 'p')
->from("Team", 't')
->leftJoin('t.players', 'p')
->setFirstResult($first)
->setMaxResults($max);
$paginator = new Paginator($qb->getQuery());
return $paginator->getIterator();
}
}
70. <?php
class FirePlayerCommand implements Command
{
public $teamId;
public $playerId;
public function getRequest()
{
return new Request(
[
'teamId' => $this->teamId,
'playerId' => $this->playerId
]
);
}
}
71. <?php
class Request extends ArrayCollection implements RequestInterface
{
public function __construct(array $values)
{
parent::__construct($values);
}
public function get($key, $default = null)
{
if (!parent::containsKey($key)) {
throw new DomainException();
}
$value = parent::get($key);
if (!$value && $default) {
return $default;
}
return $value;
}
}
72. <?php
class CommandHandler
{
private $dispatcher;
private $useCases;
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function registerUseCases($useCases)
{
foreach ($useCases as $useCase) {
if ($useCase instanceof UseCase) {
$this->useCases[$useCase->getManagedCommand()] = $useCase;
} else {
throw new LogicException('');
}
}
}
// ...}
73. <?php
class CommandHandler
{
// ...
public function execute(Command $command)
{
try {
$this->dispatcher
->dispatch(Events::PRE_COMMAND, new CommandEvent($command));
$this->useCases[get_class($command)]->run($command);
$response = new Response();
$this->dispatcher
->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response));
return $response;
} catch (DomainException $e) {
$this->dispatcher
->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e));
return new Response($e->getMessage(), Response::STATUS_KO);
}
}
}
74. <?php
class CommandHandler
{
// ...
public function execute(Command $command)
{
try {
$this->dispatcher
->dispatch(Events::PRE_COMMAND, new CommandEvent($command));
$this->useCases[get_class($command)]->run($command);
$response = new Response();
$this->dispatcher
->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response));
return $response;
} catch (DomainException $e) {
$this->dispatcher
->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e));
return new Response($e->getMessage(), Response::STATUS_KO);
}
}
}
75. <?php
class FirePlayerUseCase implements UseCase
{
private $repository;
public function __construct(TeamRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function run(Command $command)
{
$request = $command->getRequest();
$team = $this->repository->get(
$request->get('teamId')
);
$team->firePlayer(
$request->get('playerId')
);
$this->repository->add($team);
}
}
81. <?php
class MyController extends Controller
{
public function modifyLeagueAction(Request $request, $id)
{
$reader = $this->get('reader');
Consider using a service for
$league = $reader->getLeague($id);
reading $command = ModifyLeagueCommand::operations, fromArray($league);
instead
$form = $this->createForm(new ModifyLeagueType(), $command);
use $form->the handleRequest($repository request);
directly
if ($form->isValid()) {
$commandHandler = $this->get('command_handler');
$response = $commandHandler->execute($command);
if ($response->isOk()) {
//...
}
}
return array(
'form' => $form->createView()
);
}
}
82. <?php
class ModifyLeagueType extends CreateNewsType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('leagueId’, ‘’hidden')
->add('name')
->add('save', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => ModifyLeagueCommand::CLASS,
));
}
}
83. <?php
class ModifyLeagueCommand implements Command
{
public $leagueId;
public $name;
public function getRequest()
{
return new Request(
[
leagueId => $this->leagueId,
name => $this->name
]
);
}
}
A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving problem at hand and ignores extraneous detail.
il matching uno a uno tra sottodominio e bounded context è una condizione desiderabile, non un vincolo
il modello cambia spesso, tanto quanto la conoscenza che acquisiamo e che ci porta a comprendere come risolvere i problemi; TDD si sposa bene