Operation

Imagine you have a class like this:

class User
  def self.delete(id)
    @users.delete id
  end
end

What does it return? True? The User? an ID? What if an error occured? How does anyone calling User.delete 42 know what happened?

This is where Operation comes in. You would simply return an operation object. That instance holds information about what happened, like so:

class User
  def self.delete(id)
    return Operation.new(code: :id_missing) unless id
    return Operation.new(code: :invalid_id, id: id) unless id.match /[a-f]{8}/

    user = @users[id]
    if @users.delete id
      Operation.new success: true, code: :user_deleted, user: user
    else
      Operation.new code: :deletion_failed, id: id
    end

  rescue ConnectionError
      Operation.new code: :deletion_failed_badly, id: id
  end
end

There are some shortcuts too keep it less verbose:

class User
  def self.delete(id)
    return Operations.failure(:id_missing) unless id
    return Operations.failure(:invalid_id, id: id) unless id.match /[a-f]{8}/

    user = @users[id]
    if @users.delete id
      Operations.success :user_deleted, object: user
    else
      Operations.failure :deletion_failed
    end

  rescue ConnectionError
      Operation.failure :deletion_failed_badly
  end
end

So what are the benefits?

1. Robust and predictable code

This will give you this joyful, consistent, conventional, implementation-unaware programming feeling:

operation = User.delete 42

if operation.success?
  puts "It worked! You deleted the user #{operation.meta.user.first_name}"
else
  puts "Aw, could not delete User with ID #{operation.meta.id} because #{operation.code}"
end
operation = User.delete 42

operation.success? # => true
operation.failure? # => false
operation. # => { object: <#User id=42> }
operation.meta     # => { object: <#User id=42> }
operation.object   # => <#User id=42>   <- shortcut for meta[:object]

# In case you use Hashie, you will get that via #meta
require 'hashie/mash'
operation.meta     # => <#Hashie::Mash object: <#User id=42>>
operation.object   # => <#User id=42>   <- shortcut for meta.object

2. Better tests

How would you test this code?

class Product
  def self.delete(id)
    return false if id.blank?
    return false unless product = Products.find(id)
    return false unless permission?
    api.update(id, attributes)
  end

  def self.permission?
    Date.today.sunday?
  end
end

You cannot simply test for the false as expected return value because it could mean anything.

3. Documentation

While the code becomes more verbose, that verbosity translates directly into documenation. You see immediately what each line is doing.

Requirements

  • Ruby >= 2.1

MIT 2015 halo. See MIT-LICENSE.