Direct

Tell your objects what to do when things work properly or when they fail.

This allows you to encapsulate behavior inside the object. Avoid using if outside of your objects and just tell them what to do.

Usage

require 'direct'
class SomeClass
  def procedure
    Direct.defer(object: self){
      # return a truthy or falsey object
      # to execute success or failure blocks
    }
  end
end

SomeClass.new.procedure.success{ |deferred_object, result, object|
  puts "it worked!"
}.failure { |deferred_object, result, object|
  puts "it failed :-("
}.execute

If the example procedure method above raises an exception instead of just returning a falsey object, the failure block will be run.

But you can specify what to do when an exception is raised instead:

SomeClass.new.procedure.success{ |deferred_object, result, object|
  puts "it worked!"
}.failure { |deferred_object, result, object|
  puts "it failed :-("
}.exception { |deferred_object, exception, object|
  puts "Oh no! An exception was raised!"
}.execute

By default it will handle StandardError execeptions but you can be more specific if you like:

SomeClass.new.procedure.success{ |deferred_object, result, object|
  puts "it worked!"
}.failure { |deferred_object, result, object|
  puts "it failed :-("
}.exception(SomeLibrary::SomeSpecialError){ |deferred_object, exception, object|
  puts "Oh no! An exception was raised!"
}.execute

You can also handle different exceptions with different blocks:

SomeClass.new.procedure.exception(SomeLibrary::SomeSpecialError){ |deferred_object, exception, object|
  puts "Oh no! A Special Error!"
}.exception(ArgumentError){ |deferred_object, exception, object|
  puts "Oops! The arguments are wrong!"
}.execute

The defer method uses built-in classes but you can build your own to manage executing named blocks

class DeferrableClass
  include Direct

  def save
    # do stuff
    as_directed(:success, 'some', 'success', 'values')
  rescue => e
    as_directed(:failure, 'some', 'failure', e.message)
  end
end

DeferrableClass.new.direct(:success){|instance, *data|
  STDOUT.puts data
}.direct(:failure){|instance, *errors|
  STDERR.puts errors
}.save

Your blocks will always receive the object itself as the first argument.

If you want to have a better API, just make it your own:

class DeferrableClass
  def when_it_works(&)
    direct(:success, &)
  end

  def when_it_fails(&)
    direct(:oopsies, &)
  end

  def do_it
    if it_worked?
      as_directed(:success)
    else
      as_directed(:oopsies)
    end
  end
end

DeferrableClass.new.when_it_works do |instance|
  # successful path
end.when_it_fails do |instance|
  # failure path
end

Why?

You could easily write code that says if this else that.

For example:

if Something.new.save!
  puts "yay!"
else
  puts "boo!"
end

But eventually you may want more information about your successes and failures

something = Something.new
if something.save!
  puts "yay! #{something}"
else
  puts "boo! #{something}: #{something.errors}"
end

That's intially not so bad that you need to initialize the object separately from the if expression. But when we discover a third or fourth scenario, then the code can become complicated:

something = Something.new
if something.save!
  puts "yay! #{something}"
elsif something.valid? && !something.persisted?
  puts "it sort of worked"
elsif !something.valid? || something.need_some_other_thing_set?
  puts "an alternative to it not working"
else
  puts "boo! #{something}: #{something.errors}"
end

It's just too easy to expand logic and knowledge about the internal state of the object with if and else and elsif.

Instead, we can name these scenarios and allow the object to handle them; we merely provide the block of code to execute:

Something.new.direct(:success){ |obj|
    puts "yay! #{obj}"
  }.direct(:failure){ |obj, errors|
    puts "boo! #{obj}: #{errors}"
  }.direct(:other_scenario){ |obj|
    puts "here's what happened and what to do..."
  }

Inside of the object is where we can handle these named scenarios. If the meaning of :success or :failure or any other name changes, the object itself can handle it with no changes implicitly required in the calling code.

Installation

Add this line to your application's Gemfile:

gem 'direct'

And then execute:

$ bundle

Or install it yourself as:

$ gem install direct

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/saturnflyer/direct. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Direct project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.