domain-driven design in php and symfony - drupal camp wroclaw!
DESCRIPTION
Domain-driven Design in PHP and Symfony - Drupal Camp Wrocław 18/10/2014TRANSCRIPT
Domain-Driven Designin PHP & Symfony
Kacper Gunia @cakper Software Engineer @SensioLabsUK
Symfony Certified Developer
PHPers Silesia @PHPersPL
Software is complex
How to tackle it?
–Eric Evans
Domain-Driven Design is !an approach to the development of complex software in which we:
–Eric Evans
1. Focus on the core domain.
–Eric Evans
2. Explore model in !a creative collaboration of domain practitioners and software practitioners.
–Eric Evans
3. Speak an ubiquitous language within an explicitly bounded context.
When to use DDD?
Complex domain
Domain Experts
Iterative process
Object-Oriented Design
Strategic DDD
Strategy!!
Stratos - army/resources!Ago - leading
Strategy!!
!
= What
Bounded Context
Bounded contexts of product
❖ Catalogue!❖ Sales!❖ Marketing!❖ Warehouse
Ubiquitous language
Core Domain
Supporting Domain
Model-Driven Design
Continuous Integration
UI
SERVICE
UNIT
Refactoring Towards Deeper Insight
Tactical DDD
Tactics!!
Taktike - the art of organising an army, a maneuver
Tactics!!
!
= How
Building Blocks
Layered architecture
DOMAIN
USER INTERFACE
APPLICATION
INFRASTRUCTURE
. └── src └── Dcwroc ├── Shopping ├── Emejzon ├── EmejzonBundle └── DoctrineAdapter
Entities
class Sale { (…) private $id; !
function __construct($name, DateTimeInterface $dueDate) { $this-‐>name = $name; $this-‐>dueDate = $dueDate; } !
public function getId() { return $this-‐>id; } }
class Sale { (…) public function activate() { if (true === $this-‐>active) { throw new LogicException('Sale already activated!'); } !
$this-‐>active = true; } }
Value objects
class Email { (…) public function __construct($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException(); } !
$this-‐>email = $email; } !
function asString() { return $this-‐>email; } }
Aggregates
class Order {(…) function __construct( Customer $customer, ProductList $productList ){ $this-‐>customer = $customer; $this-‐>productList = $productList; $this-‐>status = Status::open(); } !
public function getTotal(); }
Repositories
interface OrderRepository { public function save(Order $order); !
public function remove(Order $order); public function findById($orderId); !
public function findObsolete(); }
class DoctrineOrderRepository implements OrderRepository {} !
class FileOrderRepository implements OrderRepository {} !
class InMemoryOrderRepository implements OrderRepository {}
Factories
class CustomerFactory { public function register(Email $email, Password $password) { (...) !
return $customer; } }
Services
interface Notifier { public function send(Message $message); }
class EmailNotifier implements Notifier { private $mailer; !
function __construct(Swift_Mailer $mailer) { $this-‐>mailer = $mailer; } !
public function send(Message $message) { (…) } }
Domain Events
class ResetPassword { function request(Email $email) { $user = $this-‐>userRepository-‐>findOneByEmail($email); !
if (!$user instanceof User) { return $this-‐>raise(new UserDoesNotExistEvent($email)); } !
$request = Password::requestReset($user); $this-‐>passwordResetRequestRepository-‐>save($request); $this-‐>raise(new RequestIssuedEvent($request)); } }
class ResetPassword { function request(Email $email) { $user = $this-‐>userRepository-‐>findOneByEmail($email); !
if (!$user instanceof User) { return $this-‐>raise(new UserDoesNotExistEvent($email)); } !
$request = Password::requestReset($user); $this-‐>passwordResetRequestRepository-‐>save($request); $this-‐>raise(new RequestIssuedEvent($request)); } }
class ResetPassword { function request(Email $email) { $user = $this-‐>userRepository-‐>findOneByEmail($email); !
if (!$user instanceof User) { return $this-‐>raise(new UserDoesNotExistEvent($email)); } !
$request = Password::requestReset($user); $this-‐>passwordResetRequestRepository-‐>save($request); $this-‐>raise(new RequestIssuedEvent($request)); } }
class ResetPassword { function request(Email $email) { $user = $this-‐>userRepository-‐>findOneByEmail($email); !
if (!$user instanceof User) { return $this-‐>raise(new UserDoesNotExistEvent($email)); } !
$request = Password::requestReset($user); $this-‐>passwordResetRequestRepository-‐>save($request); $this-‐>raise(new RequestIssuedEvent($request)); } }
Modules
. └── Shopping └── Authentication ├── InvalidPasswordResetRequest.php ├── ResetPassword.php ├── PasswordResetRequest.php └── PasswordResetRequestRepository.php
Supple Design
Make behaviour obvious
Intention revealing interfaces
Design for change
Create software other developers want
to work with
Ignore persistance
Domain model is always in a valid state
Command Pattern
class ChangeEmailCommand { public $email; public $customerId; }
class ChangeEmailCommandHandler { private $customerRepository; !
function __construct(CustomerRepository $customerRepository) { $this-‐>customerRepository = $customerRepository; } !
public function handle(ChangeEmailCommand $command) { $customer = $this-‐>customerRepository -‐>findById($command-‐>customerId); $customer-‐>changeEmail($command-‐>email); } }
Specification Pattern
interface Specification { public function isSatisfiedBy($object); }
class PayingCustomerSpecification implements Specification { public function isSatisfiedBy($object) { return $object instanceof Customer && $object-‐>getAmountSpent() > 0.0; } }
class FreeDeliveryCalculator { private $specification; !
function __construct(Specification $specification) { $this-‐>specification = $specification; } !
public function applyDiscount(Order $order) { $customer = $this-‐>$order-‐>getCustomer(); if ($this-‐>specification-‐>isSatisfiedBy($customer)) { $order-‐>deliverForFree(); } } }
class FreeDeliveryCalculator { private $specification; !
function __construct(Specification $specification) { $this-‐>specification = $specification; } !
public function applyDiscount(Order $order) { $customer = $this-‐>$order-‐>getCustomer(); if ($this-‐>specification-‐>isSatisfiedBy($customer)) { $order-‐>deliverForFree(); } } }
Command-Query Responsibility Segregation
Commands
Use ORM to fetch/save aggregate roots
Queries
Use specialised queries and DTO’s to fetch presentation data
DDD Summary
Explore Domain Knowledge
With Domain Experts
By speaking Ubiquitous Language
Make use of Building Blocks
By applying Object-Oriented Design
Design for change
That’s the only constant!
–Eric Evans
“In order to create good software, you have to
know what that software is all about.”
Kacper Gunia Software Engineer
Symfony Certified Developer
PHPers Silesia
Thanks!