AyeCommander

Gem Version Build Status Code Climate Test Coverage

Requirements

  • Ruby version >= 2.0

Installation

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'

Introduction

AyeCommander is a gem that helps to develop classes that follow the command pattern.

What is a Command?

A command is an object that does nothing but wait to be executed and, when executed, goes out and performs an application-specific task.

— Russ Oslen

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?

When to use a Command

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

When to NOT use a Command

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!

The Command

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.

Limiting the arguments

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.

Receiving Arguments

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.

Returning Arguments

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.

Limiters Examples

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)

What’s in a status?

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)

Multiple succeeds

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

Abort!

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>

Getting Hooked

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.

Before Hooks

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

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!

Aborted Hooks

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.

Around Hooks

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

Aye Aye Commander!

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.

Run and Done

Similarly to the command, on its simplest form you only need to do two things to use a commander.

  • Include AyeCommander::Commander, not AyeCommander::Command

  • Use execute with the Command 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>]>

Commander Result

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>

Aborting and Failing

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>]>

When we need more tweaking

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

Top tips and tricks

  • 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 with Command. 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!

License

AyeCommander is released under the MIT License.