command-oriented architecture

70
Command-Oriented Architecture Maceió DEV Meetup #6

Upload: luiz-messias

Post on 05-Aug-2015

70 views

Category:

Software


1 download

TRANSCRIPT

Command-Oriented Architecture

Maceió DEV Meetup #6

who am I?

➔ Tony Messias ~ @tony0x01

➔ Building web stuff since ~2010

before we start...

➔ CRUD thinking

➔ MVC

➔ Commands/Events

➔ Clean Architecture

“CRUD is an antipattern”

(Mathias Verraes)

“CRUD doesn't express behaviour. Avoid setters, and use expressive, encapsulated operations instead.”

<?php

$order = new Order();

$order->setStatus('paid');

$order->setPaidAmount(120);

$order->setPaidCurrency('EUR');

$order->setCustomer($customer);

<?php

$order = new Order();

$money = new Money(120, new Currency('EUR'));

$order->pay($customer, $money);

class CommentsController extends Controller {

public function store($postId)

{

$post = Post::find($postId);

$comment = new Comment([

'message' => 'A new comment.',

'user_id' => Auth::user()->id

]);

$post->comments()->save($comment);

return redirect()

->route('posts.view, $post)

->withMessage('Your comment was successfully created');

}

}

class CommentsController extends Controller {

public function store($postId)

{

$post = Post::find($postId);

$comment = new Comment([

'message' => 'A new comment.',

'user_id' => Auth::user()->id

]);

$post->comments()->save($comment);

return redirect()

->route('posts.view, $post)

->withMessage('Your comment was successfully created');

}

}

class CommentsController extends Controller {

public function store($postId)

{

$post = Post::find($postId);

$comment = new Comment(['message' => 'A new comment.']);

$user = Auth::user();

$post->comment($user, $comment);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

class CommentsController extends Controller {

public function store($postId)

{

$post = Post::find($postId);

$comment = new Comment(['message' => 'A new comment.']);

$user = Auth::user();

$post->comment($user, $comment);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

class Post extends Model

{

// ...

public function comment(User $user, Comment $comment)

{

$comment->user_id = $user->id;

$this->comments()->save($comment);

}

// ...

}

class SendSMS {

public function fire($job, $data)

{

$twilio = new Twilio_SMS($apiKey);

$twilio->sendTextMessage(array(

'to' => $data['user']['phone_number'],

'message' => $data['message'],

));

$user = User::find($data['user']['id']);

$user->messages()->create([

'to' => $data['user']['phone_number'],

'message' => $data['message'],

]);

$job->delete();

}

}

class SendSMS {

public function fire($job, $data)

{

$twilio = new Twilio_SMS($apiKey);

$twilio->sendTextMessage(array(

'to' => $data['user']['phone_number'],

'message' => $data['message'],

));

$user = User::find($data['user']['id']);

$user->messages()->create([

'to' => $data['user']['phone_number'],

'message' => $data['message'],

]);

$job->delete();

}

}

class SendSMS {

public function fire($job, $data)

{

$twilio = new Twilio_SMS($apiKey);

$twilio->sendTextMessage(array(

'to' => $data['user']['phone_number'],

'message' => $data['message'],

));

$user = User::find($data['user']['id']);

$user->messages()->create([

'to' => $data['user']['phone_number'],

'message' => $data['message'],

]);

$job->delete();

}

}

class SendSMS {

function __construct(UserRepository $users, SmsCourierInterface $courier)

{

$this->users = $users;

$this->courier = $courier;

}

public function fire($job, $data)

{

$user = $this->users->find($data['user']['id']);

$user->sendSmsMessage($this->courier, $data['message']);

$job->delete();

}

}

use Illuminate\Database\Eloquent\Model;

class User extends Model

{

public function sendSmsMessage(SmsCourierInterface $courier, $message)

{

$courier->sendMessage($this->phone_number, $message);

return $this->messages()->create([

'to' => $this->phone_number,

'message' => $message,

]);

}

}

class SmsTest extends PHPUnit_Framework_TestCase {

public function test_user_can_send_sms_message() {

$user = Mockery::mock('User[messages]');

$relation = Mockery::mock('StdClass');

$courier = Mockery::mock('SmsCourierInterface');

$user->shouldReceive('messages')->once()->andReturn($relation);

$relation->shouldReceive('create')->once()->with(array(

'to' => '555-555-5555',

'message' => 'Test',

));

$courier->shouldReceive('sendMessage')->once()->with(

'555-555-5555', 'Test'

);

$user->phone_number = '555-555-5555';

$user->sendSmsMessage($courier, 'Test');

}

}

class SmsTest extends PHPUnit_Framework_TestCase {

public function test_user_can_send_sms_message() {

$user = Mockery::mock('User[messages]');

$relation = Mockery::mock('StdClass');

$courier = Mockery::mock('SmsCourierInterface');

$user->shouldReceive('messages')->once()->andReturn($relation);

$relation->shouldReceive('create')->once()->with(array(

'to' => '555-555-5555',

'message' => 'Test',

));

$courier->shouldReceive('sendMessage')->once()->with(

'555-555-5555', 'Test'

);

$user->phone_number = '555-555-5555';

$user->sendSmsMessage($courier, 'Test');

}

}

class SmsTest extends PHPUnit_Framework_TestCase {

public function test_user_can_send_sms_message() {

$user = Mockery::mock('User[messages]');

$relation = Mockery::mock('StdClass');

$courier = Mockery::mock('SmsCourierInterface');

$user->shouldReceive('messages')->once()->andReturn($relation);

$relation->shouldReceive('create')->once()->with(array(

'to' => '555-555-5555',

'message' => 'Test',

));

$courier->shouldReceive('sendMessage')->once()->with(

'555-555-5555', 'Test'

);

$user->phone_number = '555-555-5555';

$user->sendSmsMessage($courier, 'Test');

}

}

class SmsTest extends PHPUnit_Framework_TestCase {

public function test_user_can_send_sms_message() {

$user = Mockery::mock('User[messages]');

$relation = Mockery::mock('StdClass');

$courier = Mockery::mock('SmsCourierInterface');

$user->shouldReceive('messages')->once()->andReturn($relation);

$relation->shouldReceive('create')->once()->with(array(

'to' => '555-555-5555',

'message' => 'Test',

));

$courier->shouldReceive('sendMessage')->once()->with(

'555-555-5555', 'Test'

);

$user->phone_number = '555-555-5555';

$user->sendSmsMessage($courier, 'Test');

}

}

be careful with MVC

your framework is not your architecture

$ tree rails/app

rails/app

├── assets

├── controllers

├── helpers

├── mailers

├── models

└── views

“this is a rails app”

Screaming Architecture

ok, but what does it have to do with Commands?

they are basically DTOs,with cool names

class CommentsController extends Controller

{

public function store($postId)

{

$user = Auth::user();

$post = Post::find($postId);

$comment = new Comment(['message' => 'A new comment.']);

$post->comment($user, $comment);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

class CommentsController extends Controller

{

public function store($postId)

{

$user = Auth::user();

$post = Post::find($postId);

$comment = new Comment(['message' => 'A new comment.']);

$post->comment($user, $comment);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

class CommentsController extends Controller

{

public function store($postId)

{

$user = Auth::user();

$post = Post::find($postId);

$comment = new Comment(['message' => 'A new comment.']);

$post->comment($user, $comment);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

class CommentsController extends Controller

{

public function store($postId)

{

$user = Auth::user();

$message = Input::get('message');

$command = new LeaveCommentCommand($user, $postId, $message);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

class LeaveCommentCommand

{

public $user;

public $postId;

public $message;

public function __construct(User $user, $postId, $message)

{

$this->user = $user;

$this->postId = $postId;

$this->message = $message;

}

}

how do I execute them?

class CommentsController extends Controller

{

public function store($postId)

{

$user = Auth::user();

$message = Input::get('message');

$command = new LeaveCommentCommand($user, $postId, $message);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

use Illuminate\Foundation\Bus\DispatchesCommands;

class CommentsController extends Controller {

use DispatchesCommands;

public function store($postId) {

$user = Auth::user();

$message = Input::get('message');

$command = new LeaveCommentCommand($user, $postId, $message);

$this->dispatch($command);

return redirect()

->route('posts.index', $post)

->withMessage('Your comment was successfully created');

}

}

what does dispatch do?

finds a handler for our command

one Command can be executed by one and

only one Handler

LeaveCommentCommand

LeaveCommentCommandHandler

class LeaveCommentCommandHandler

{

public function handle(LeaveCommentCommand $command)

{

$post = Post::find($command->postId);

$comment = new Comment(['message' => $command->message]);

$post->comment($command->user, $comment);

}

}

what if I want to notify the post creator about

that new comment?

class LeaveCommentCommandHandler {

private $mailer;

function __construct(UserMailer $mailer) {

$this->mailer = $mailer;

}

public function handle(LeaveCommentCommand $command) {

$post = Post::find($command->postId);

$comment = new Comment(['message' => $command->message]);

$post->comment($command->user, $comment);

$this->notifyPostCreator($post->creator, $post, $comment);

}

// ...

}

class LeaveCommentCommandHandler

{

// ...

private function notifyPostCreator(

User $creator, Post $post, Comment $comment)

{

$this->mailer->sendTo(

$creator->email,

sprintf("New comment on [%s]", $post->title),

sprintf("User @%s left a comment for you: \n%s",

$comment->user->username,

$comment->message)

);

}

}

works, but we can do better...

use Illuminate\Contracts\Events\Dispatcher;

class LeaveCommentCommandHandler {

private $events;

function __construct(Dispatcher $events) {

$this->events = $events;

}

public function handle(LeaveCommentCommand $command) {

$post = Post::find($command->postId);

$comment = new Comment(['message' => $command->message]);

$post->comment($command->user, $comment);

$this->dispatchEvents($post->releaseEvents());

}

// ...

}

use Illuminate\Contracts\Events\Dispatcher;

class LeaveCommentCommandHandler {

// ...

private function dispatchEvents(array $events)

{

foreach ($events as $event)

$this->events->fire($event);

}

}

class Post extends Model

{

use EventGenerator;

public function comment(User $user, Comment $comment)

{

$comment->user_id = $user->id;

$this->comments()->save($comment);

$this->raise(new CommentWasLeft($post, $comment, $user));

}

}

trait EventGenerator

{

protected $domainEvents = [];

public function raise($event)

{

$this->domainEvents[] = $event;

}

public function releaseEvents()

{

$events = $this->domainEvents;

$this->domainEvents = [];

return $events;

}

}

events are also just DTOs

class CommentWasLeft

{

public $post;

public $user;

public $comment;

public function __construct(Post $post, User $user, Comment $comment)

{

$this->post = $post;

$this->user = $user;

$this->comment = $comment;

}

}

but they can (and most of the time they do)

have lots of listeners/handlers

class NotifyPostOwnerAboutNewCommentHandler {

private $mailer;

function __construct(UserMailer $mailer) {

$this->mailer = $mailer;

}

public function handle(CommentWasLeft $event) {

$this->mailer->sendTo(

$event->post->creator->email,

sprintf("New comment on [%s]", $event->post->title),

sprintf("User @%s left a comment for you: \n%s",

$event->user->username, $event->comment->message)

);

}

}

class EventServiceProvider extends ServiceProvider

{

/**

* The event handler mappings for the application.

* @param array

*/

protected $listen = [

CommentWasLeft::class => [

NotifyPostOwnerAboutNewCommentHandler::class

]

];

}

Recap:

➔ Boundaries interacts through commands;

➔ Command is executed by its handler;

➔ Command handlers fires/triggers domain

events;

➔ Events are listened by event handlers/listeners.

$ tree app

app

├── Commands

├── Console

├── Events

├── Exceptions

├── Handlers

├── Http

├── Providers

├── Services

└── User.php

$ tree app/Commands

app/Commands

├── Command.php

└── LeaveCommentCommand.php

$ tree app/Handlers

app/Handlers

├── Commands

│ └── LeaveCommentCommandHandler.php

└── Events

└── NotifyPostOwnerAboutNewCommentHandler.php

avoid CRUD thinking

$ tree app/Commands

app/Commands

├── CreateUserCommand.php

└── DeleteUserCommand.php

└── UpdateUserCommand.php

class DeactivateInventoryItemCommand

{

public $userId;

public $itemId;

public $comment;

public function __construct($userId, $itemId, $comment)

{

$this->userId = $userId;

$this->itemId = $itemId;

$this->comment = $comment;

}

}

you can easily use queues to speed up your

requests.

use Illuminate\Contracts\Queue\ShouldBeQueued;

class DeactivateInventoryItemCommand implements ShouldBeQueued {

public $userId;

public $itemId;

public $comment;

public function __construct($userId, $itemId, $comment) {

$this->userId = $userId;

$this->itemId = $itemId;

$this->comment = $comment;

}

}

use Illuminate\Contracts\Queue\ShouldBeQueued;

class NotifyPostOwnerAboutNewCommentHandler implements ShouldBeQueued {

private $mailer;

function __construct(UserMailer $mailer) {

$this->mailer = $mailer;

}

public function handle(CommentWasLeft $event) {

$this->mailer->sendTo(

$event->post->creator->email,

sprintf("New comment on [%s]", $event->post->title),

sprintf("User @%s left a comment for you: \n%s",

$event->user->username, $event->comment->message)

);

}

}

questions?