Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Command-Oriented Architecture

Command-Oriented Architecture

Talk I gave at Maceió DEV Meetup #6. Not only about Command Bus/Command Interface or whatever you name it, but a compilation of cool articles I found that may help with understanding this architecture.

Resources and links:

- Command Bus by Shawn Mccool http://shawnmc.cool/command-bus
- Dev Discussions - The Command Bus https://www.youtube.com/watch?v=fbSYZFZCFS0
- Screaming Archirecture by Uncle Bob http://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html
- The Clean Archirecture by Uncle Bob http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
- Laravel: From Apprentice to Artisan https://leanpub.com/laravel
- Commands and Domain Events (Laracasts) https://laracasts.com/series/commands-and-domain-events
- Task-based UIs https://cqrs.wordpress.com/documents/task-based-ui/
- CRUD is an antipattern by Mathias Verraes http://verraes.net/2013/04/crud-is-an-anti-pattern/

Tony Messias

April 24, 2015
Tweet

More Decks by Tony Messias

Other Decks in Programming

Transcript

  1. <?php $order = new Order(); $money = new Money(120, new

    Currency('EUR')); $order->pay($customer, $money);
  2. 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'); } }
  3. 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'); } }
  4. 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'); } }
  5. 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'); } }
  6. class Post extends Model { // ... public function comment(User

    $user, Comment $comment) { $comment->user_id = $user->id; $this->comments()->save($comment); } // ... }
  7. 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(); } }
  8. 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(); } }
  9. 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(); } }
  10. 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(); } }
  11. 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, ]); } }
  12. 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'); } }
  13. 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'); } }
  14. 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'); } }
  15. 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'); } }
  16. 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'); } }
  17. 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'); } }
  18. 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'); } }
  19. 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'); } }
  20. class LeaveCommentCommand { public $user; public $postId; public $message; public

    function __construct(User $user, $postId, $message) { $this->user = $user; $this->postId = $postId; $this->message = $message; } }
  21. 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'); } }
  22. 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'); } }
  23. class LeaveCommentCommandHandler { public function handle(LeaveCommentCommand $command) { $post =

    Post::find($command->postId); $comment = new Comment(['message' => $command->message]); $post->comment($command->user, $comment); } }
  24. 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); } // ... }
  25. 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) ); } }
  26. 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()); } // ... }
  27. 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)); } }
  28. trait EventGenerator { protected $domainEvents = []; public function raise($event)

    { $this->domainEvents[] = $event; } public function releaseEvents() { $events = $this->domainEvents; $this->domainEvents = []; return $events; } }
  29. 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; } }
  30. but they can (and most of the time they do)

    have lots of listeners/handlers
  31. 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) ); } }
  32. class EventServiceProvider extends ServiceProvider { /** * The event handler

    mappings for the application. * @param array */ protected $listen = [ CommentWasLeft::class => [ NotifyPostOwnerAboutNewCommentHandler::class ] ]; }
  33. Recap: ➔ Boundaries interacts through commands; ➔ Command is executed

    by its handler; ➔ Command handlers fires/triggers domain events; ➔ Events are listened by event handlers/listeners.
  34. $ tree app app ├── Commands ├── Console ├── Events

    ├── Exceptions ├── Handlers ├── Http ├── Providers ├── Services └── User.php
  35. $ tree app/Commands app/Commands ├── Command.php └── LeaveCommentCommand.php $ tree

    app/Handlers app/Handlers ├── Commands │ └── LeaveCommentCommandHandler.php └── Events └── NotifyPostOwnerAboutNewCommentHandler.php
  36. class DeactivateInventoryItemCommand { public $userId; public $itemId; public $comment; public

    function __construct($userId, $itemId, $comment) { $this->userId = $userId; $this->itemId = $itemId; $this->comment = $comment; } }
  37. 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; } }
  38. 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) ); } }
  39. Resources ➔ Command Bus by Shawn Mccool ➔ Dev Discussions

    - The Command Bus ➔ Screaming Archirecture by Uncle Bob ➔ The Clean Archirecture by Uncle Bob ➔ Laravel: From Apprentice to Artisan
  40. Resources ➔ Commands and Domain Events (Laracasts) ➔ Task-based UIs

    ➔ CRUD is an antipattern by Mathias Verraes