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..user.first_name}"
else
puts "Aw, could not delete User with ID #{operation..id} because #{operation.code}"
end
operation = User.delete 42
operation.success? # => true
operation.failure? # => false
operation. # => { object: <#User id=42> }
operation. # => { 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. # => <#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
api.update(id, attributes)
end
def self.
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
Copyright
MIT 2015 halo. See MIT-LICENSE.