To use AyeCommander add it to your Gemfile:
gem 'aye_commander'
And bundle!
bundle install
Or to use without bundler, install the gem:
gem install aye_commander
And then require it from your code
require 'aye_commander'
AyeCommander is a gem that helps to develop classes that follow the command pattern.
A command is an object that does nothing but wait to be executed and, when executed, goes out and performs an application-specific task.
Simply put, a command is an object that does but one thing. So if only does one thing… why would you need to use them?
Let’s imagine that we have to do a complicated operation in a web application, like charging money. Just the charging alone might involved consuming one or more services to authorize and charge the card, save several records with information about the payments and so on and so forth.
Writing all this code in a model is not exactly correct since it handles way more than just one model and using a controller would not only make a fat controller, but also harder to read.
If we instead write all this logic in one (or more) commands, the code becomes not only easier to read and understand, but also easier to reuse on a different context.
# Instead of letting the model handle more responsability than it should
class Order
def create_order
charge_card
save_payment
update_order
end
end
# Or polluting the controller with more than just, "How to respond to the end user?"
class OrdersController
def create
charge_card
save_payment
update_order
flash[:notice] = "Everything went well"
end
end
# Why not extract it all a command
class CheckoutOrderCommand
include AyeCommander::Command
def call
charge_card
save_payment
update_order
end
end
# Or maybe even several commands
class CheckoutOrderCommand
include AyeCommander::Commander
execute ChargeCardCommand, SavePaymentCommand, UpdateOrderCommand
end
As stated before a command is an object that does one thing.
This simple definition may make it tempting to write commands left and right, but never forget that
you need to KISS. If what you’re trying to do is
simple it doesn’t really need be extracted into a command.
Ok, lets get cracking!
Creating a command is really easy, you only need to do two things to get rocking:
-
Include the
AyeCommander::Command
module -
Define a method named
call
class ObtainRandomCommand
include AyeCommander::Command
def call
@random = array.sample
end
end
To use the command, you simply call it from somewhere else.
result = ObtainRandomCommand.call(array: [1, 2, 3])
=> #<ObtainRandomCommand::Result @status: success, @array: [1, 2, 3], @random: 3>
result.random
=> 3
It really doesn’t get simpler than that, but there’s actually more to a command than that, so lets have a look at the more complicated parts.
As you keep working with commands, you may realize that’s actually a bit complicated to know what a command expects to receive as arguments, what’s the minimum necessary it needs to work and which of all the variables returned in the result are actually relevant to you.
AyeCommander comes with two ways of limiting the arguments that your command needs to be able to
run: requires
and receives
.
A requires
tells the command that it can’t run properly without having said arguments so it will
in fact raise a MissingRequiredArgumentError
if the command is called without said arguments.
A receives
tells the command that it can ONLY run the command with that set of arguments, and
that receiving any extra is actually an error. In this case if a command receives any surplus, an
error is raised.
Arguments in requires
are automatically added to receives
, but no exception error is raised
unless you actually use a receives
.
All validations can be skipped by sending the :skip_validations
option when calling the command.
So now that your command ran, your result might end up with a bunch of variables that you may
actually not even need. If that’s the case then you can use the returns
method which as you might
imagine, cleans up the result by just returning the variables that you specified.
class SimpleCommand
include AyeCommander::Command
end
# At this point, our command will receive and return everything and anything.
SimpleCommand.call(something: :or, other: :var)
=> #<SimpleCommand::Result @status: success, @something: or, @other: var>
class SimpleCommand
requires :these, :two
end
# Now calling the command without :these and :two will raise an error
SimpleCommand.call
=> AyeCommander::MissingRequiredArgumentError: Missing required arguments: [:these, :two]
SimpleCommand.call(these: 1, two: 2)
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2>
# Adding any extras at this point is still ok!
SimpleCommand.call(these: 1, two: 2, three: 3)
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2, @three: 3>
class SimpleCommand
receives :four
end
# Now that a receives has been used, any extra arguments sent will raise an error
SimpleCommand.call(these: 1, two: 2, three: 3)
=> AyeCommander::UnexpectedReceivedArgumentError: Received unexpected arguments: [:three]
SimpleCommand.call(these: 1, two: 2, four: 4)
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2, @four: 4>
# Not sending something that is on the receives is ok as well!
SimpleCommand.call(these: 1, two: 2)
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2>
class SimpleCommand
returns :sum
def call
@sum = these + two
end
end
# Finally a returns will help clean up the result at the end!
SimpleCommand.call(these: 1, two: 2, four: 4)
=> #<SimpleCommand::Result @status: success, @sum: 3>
# At any point you can override the receives requires or returns.
# Skips receives and requires
SimpleCommand.call(skip_validations: true)
# Skips either
SimpleCommand.call(skip_validations: :receives)
SimpleCommand.call(skip_validations: :requires)
# Skips result cleanup
SimpleCommand.call(skip_cleanup: true)
As you may have noticed by now, every time a command is called a status
is returned regardless
of whether or not we cleanup. So what exactly is a status?
Well, at its simplest form the status tells us the whether or not the command has succeeded. By
default a command will be successful, and will fail if you change the status to ANYTHING that’s
not :success
.
class ReactorStatusCommand
include AyeCommander::Command
def call?
success? # => true
@status = :meltdown
success? # => false
end
end
ReactorStatusCommand.call.failure?
=> true
As a side note you can use the fail!
method to fail the command at any point.
def call
# These lines are functionally identical
@status = :failure
fail!
# So are these
@status = :meltdown
fail!(:meltdown)
end
Note
|
Failing a command WILL NOT stop the rest of the code from running. (More on that later) |
Up to this point the status may seem a bit bland… And you may be right!
A status can tell you more than just a simple suceed and fail! It can tell you how it succeeded or
how it failed. Doing this with failures is fairly easy, since anything that’s not :success
is
considered a failure, but how do you we add more statuses as successes?
class CreateUserTokenCommand
include AyeCommander::Command
succeeds_with :previously_created
def call
status # => :success
if user.token.present?
@status = :previously_created
success? # => true
else
user.create_random_token
fail!(:token_not_created) if user.token.blank?
end
end
end
This contrived example hopefully helps you understand when multiple success status can be useful. In fact, you can actually even exclude success from the successful status. If you do, the status will be initialized as the first in your successful statuses.
class ProcessCommand
include AyeCommander::Command
succeeds_with :started, :progress, :complete, exclude_success: true
def call
status # => :started
do_something
@status = :progress
do_something_else
@status = everything_ok? ? :complete : :failure
end
end
Now let’s imagine that at point in time you want stop running the command. Not necessarily because something went wrong, but you don’t need to do anything more for the time being. What can you do?
Well the most obvious (and possibly more correct) answer is you can use return
to exit out of the
flow. However at times you may define other methods in a command you kinda wish to exit from them,
something you can’t do with a return.
def call
do_something
# A return may work here
return if status == :cant_do_next
end
private
def do_something
# But it doesn't work if you want to use it from here instead
return if status == :cant_do_next
end
To solve this problem, command has a method named #abort!
.
Calling abort will stop the command on it’s trails and will immediately return the result. It WILL
NOT change the status so if you need change or fail the status, do it before aborting.
class ProcessCommand
include AyeCommander::Command
succeeds_with :processed
def call
do_something
# These lines will never be called
do_something_else
end
private
def do_something
if true
@status = :processed
abort!
end
end
def do_something_else
@status = :something_else
end
end
ProcessCommand.call
=> #<SimpleCommand::Result @status: processed>
A command also comes with your standard set of before, around and after hooks to tweak the command. Additionaly commands come bundled with a fourth kind of hook, the aborted hook. The easiest way to understand them, it to see the order of execution of a command.
# Rough representation of your typical call command
def call
initialize_command
validate_args
before_hooks
around_hooks { call_command }
after_hooks
aborted_hooks if aborted
return_result
end
Before going deeper into each kind of hook it’s worth mentioning the behavior which all hooks share:
-
All hooks can be declared either using a block, a symbol, a proc or a lambda.
-
Multiple hooks of the same kind can be declared, they will always be run from the first one that was declared to the last one.
-
If you need a hook to be run before some that have already been declared, you can use the
prepend: true
option. -
It might be obvious but worth noting that hooks are run in the command instance; as such you have access to everything the command has.
# Basic hook order
before do
# I run first!
# If I wanted, I could abort the rest of the command from here!
end
before :my_hook
lambda_from_somewhere_else = -> { "I run third!" }
before lambda_from_somewhere_else
private
def my_hook
# I run second
end
# More complicated hook behavior
after :third do
# fourth
end
after :first, :second, prepend: true
Important
|
Just because there’s a lot of liberty with hook order it doesn’t mean that its
recommended to abuse it. Always try to keep the order of your hooks clear, and use prepend only
if you NEED to.
|
The most important thing to note of before hooks is that while indeed they’re called before the command, they’re also called AFTER the validations have run. This is important because it does mean that you if your command requires any arguments they can’t be added through a before hook.
While it was possible to make the before hooks run before the validations this decision was taken
because requires
and receives
are meant to be ARGUMENT validators. This also means a couple of
things:
-
Receives and requires become a way to tell the users of your command how to use it properly
-
When a validator error is raised you always know it’s because of the arguments you sent
After hooks are the easiest to understand. They run after your command was called, but before the result is created, so if you need to tweak your results a bit you can do it in here!
As you might imagine, these hooks are only run if you abort the command. Why do we need them in the
first place? Well as you may remember, calling abort!
will stop the command on its tracks and
return the result immediately. This means that if you call abort!
during call
, after_hooks
WILL NOT run. For these cases, you might want to use an abort hook instead.
Oh man, around hooks. It seems that every time I see an implementation of around hooks they work in a different way, so it’s kinda hard to standarize them.
Around hooks in a command are sadly no different, as they just try to make sense.
First things first, when you use an around hook you must compromise to ALWAYS be able to receive an object and call it at some point in your method/block. If you don’t, your command will never be called.
Now, when there are multiple around hooks the first one will call the second one and so forth until
the command is called. This means that before the call
the code is run in the order the arounds
were, but after the call
it is run in the REVERSE order.
Always keep this in mind.
around do |next_step|
puts "First before call"
next_step.call
puts "First after call"
end
around do |next_step|
puts "Second before call"
next_step.call
puts "Second after call"
end
def call
puts "Command called"
end
# Would output:
=> First before call
=> Second before call
=> Command called
=> Second after call
=> First after call
I’ve been waiting this whole README to write that.
A commander is actually a command which task is to run other commands. There are two ways to do this so lets start with the simpler one.
Similarly to the command, on its simplest form you only need to do two things to use a commander.
-
Include
AyeCommander::Commander
, notAyeCommander::Command
-
Use
execute
with theCommand
s you want to be runned.
Calling the commander will run the commands one by one… and that’s pretty much it.
class Palpatine
include AyeCommander::Commander
execute HelpRepublic, Order66, BuildEmpire
end
Palpatine.call
=> #<Palpatine::Result @status: success, @executed: [#<HelpRepublic @status: success>, #<Order66 @status: success>, #<BuildEmpire @status: success>]>
As you may have noticed, the commander result not only includes a status, but also an array with the instances of the command that were run. Handy!
The commander result will not only contain this set of variables; at the end it will take all the variables that were present on the last executed command. Which brings us to an important point: commands run by the commander ALWAYS skip both cleanup and receives validations (requires are still run).
This is done so that the complete set of variable is sent to the next command to be run. If you want to cleanup the commander, you must declare its own set of returns.
class BadgerCommand
include AyeCommander::Command
returns :badger
end
class TheCommander
include AyeCommander::Commander
end
# Notice how the command returns is ignored
TheCommander.call(extra: :params)
=> #<TheCommander::Result @status: success, @executed: [...], @extra: params>
class TheCommander
returns :extra
end
# With returns defined, commander now cleans up the result
TheCommander.call(extra: :params)
=> #<TheCommander::Result @status: success, @extra: params>
So what happens when the command we’re running aborts? Absolutely Nothing! Remember that we can abort! on success, so a commander doesn’t really cares.
On the other hand if the command we’re running fails the commander itself will fail and abort.
class Palpatine
include AyeCommander::Commander
execute HelpRepublic, Order66, BuildEmpire
end
# If Order66 were to fail
Palpatine.call
=> #<Palpatine::Result @status: failure, @executed: [#<HelpRepublic @status: success>, #<Order66 @status: jedi_escaped>]>
Now, while executing several commands in a row is nice, sometimes you need a bit more of control on when to run command A or B.
Don’t worry, AyeCommander has you covered! The only thing you need to do is define your own call method!
class PickyCommander
include AyeCommander::Commander
def call
execute FirstCommand
if command.failure?
execute ThisCommand, ThatCommand
else
execute AnotherCommand
end
end
end
There are a couple of things that we must notice here.
First of all, the command
instance variable. This variable will always have the last command that
was executed. If no command has been run yet, it will have an anonymous command instance to which
you can add extras for the following commands to run.
before do
command.extra_arg = 'This extra arg'
end
after do
command.some_other = 'This' if command.that.blank?
end
def call
# Command instance will have extra_arg available
execute Command
# Commander Result will have some_other if that is blank after running Command
end
Important
|
The command variable is available for BOTH kinds of commanders, so you can use it to
prepare and finalize your commander. This marks the biggest difference between a Commander and a
Command . While everything in a command operates on it’s own instance, a commander operates over
the instance of the commands it executes.
|
The second thing to notice is that as opposed to their simple counterpart, the commander DOES NOT abort nor fail when one of the commands you run fails. This is done so you can tweak the behavior of the commander to your necessities, however recognizing that it is quite likely that you want that behaviour for your commander there are ways to reenable it.
class UndecisiveCommander
include AyeCommander::Commander
# Using this will re-enable failing on all commands
abort_on_failure
def call
# But even with that option, you override it at an instance level
# Will always abort on failure
execute ThisCommand, abort_on_failure: true
# Will never abort on failure
execute ThatCommand, OtherCommand, abort_on_failure: false
end
end
-
Never forget when and when not to use a command
-
Have naming conventions
I really suggest that for commands (and commanders), you finish their names withCommand
. This clears up what they are and maybe what they do just by looking at the name. -
Use private methods to know what your command does at first glance
class UpdateExchangeRatesCommand
include AyeCommander::Command
def call
fetch_todays_exchange_rates
save_exchange_rates
end
end
-
But if the logic is too complicated, split it into more commands
class UpdateExchangeRatesCommand
include AyeCommander::Commander
execute FetchExchangeRatesCommand, SaveExchangeRates
end
-
Write code, have fun!
AyeCommander is released under the MIT License.