pyzlnar/aye_commander

View on GitHub
README.adoc

Summary

Maintainability
Test Coverage
// Asciidoctor Source
// AyeCommander README
//
// Original author:
// - pyzlnar
//
// Notes:
// Compile with: $ asciidoctor README.adoc

= AyeCommander
[A small command gem]
:toc:
:showtitle:
:source-highlighter: coderay

image:https://badge.fury.io/rb/aye_commander.svg["Gem Version", link="https://badge.fury.io/rb/aye_commander"]
image:https://travis-ci.org/pyzlnar/aye_commander.svg?branch=master["Build Status", link="https://travis-ci.org/pyzlnar/aye_commander"]
image:https://codeclimate.com/github/pyzlnar/aye_commander/badges/gpa.svg["Code Climate", link="https://codeclimate.com/github/pyzlnar/aye_commander"]
image:https://codeclimate.com/github/pyzlnar/aye_commander/badges/coverage.svg["Test Coverage", link="https://codeclimate.com/github/pyzlnar/aye_commander/coverage"]

== Requirements

- Ruby version >= 2.0

== Installation

To use AyeCommander add it to your Gemfile:

[source,ruby]
gem 'aye_commander'

And bundle!

[source,ruby]
bundle install

Or to use without bundler, install the gem:

[source,ruby]
gem install aye_commander

And then require it from your code

[source,ruby]
require 'aye_commander'

== Introduction

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

=== What is a Command?

[quote, Russ Oslen]
____
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?

=== 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.

[source,ruby]
----
# 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 https://en.wikipedia.org/wiki/KISS_principle[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`

[source,ruby]
----
class ObtainRandomCommand
  include AyeCommander::Command

  def call
    @random = array.sample
  end
end
----

To use the command, you simply call it from somewhere else.

[source,ruby]
----
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

[source,ruby]
----
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`.

[source,ruby]
----
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.
[source,ruby]
----
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?

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
# 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.

[source,ruby]
----
# 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
----

[source,ruby]
----
# 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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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!

[source,ruby]
----
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.

[source,ruby]
----
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.

[source,ruby]
----
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 +

[source,ruby]
----
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

[source,ruby]
----
class UpdateExchangeRatesCommand
  include AyeCommander::Commander
  execute FetchExchangeRatesCommand, SaveExchangeRates
end
----

- Write code, have fun!

== License

AyeCommander is released under the https://opensource.org/licenses/MIT[MIT License].