Skip to content

Instantly share code, notes, and snippets.

@reu
Created May 21, 2012 14:36
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save reu/2762650 to your computer and use it in GitHub Desktop.
Save reu/2762650 to your computer and use it in GitHub Desktop.
Ruby annotations

Imagine this case:

class Account < ActiveRecord::Base
  def transfer(other, quantity)
    tries = 0
    begin
      tries += 1
      transaction do
        self.quantity -= quantity
        other.quantity += quantity
        self.save!
        other.save!
      end
    rescue
      retry if tries < 5
      raise
    end
  end
end

The method has lots of "noise", it is difficult to see the business rules with all the control flow on the way.

The idea is to extract most of the things that are not really business rules to separated classes, and then decorate the method with the selected extractions.

For instance, let's extract the "Transaction" part of the method:

class Transactional < Annotation
  def call(method, *args, &block)
    ActiveRecord::Base.transaction do
      method.call(*args, &block)
    end
  end
end

Then we can decorate the method like this (note that the Retry decorator is already defined by the gem):

class Account < ActiveRecord::Base
  extend Annotations

  +Retry.new(5)
  +Transactional
  def transfer(other, quantity)
    self.quantity -= quantity
    other.quantity += quantity
    self.save!
    other.save!
  end
end
Gem::Specification.new do |s|
s.name = "annotations"
s.summary = "Annotations for ruby yay!"
s.description = "Annotations for ruby yay!"
s.version = "0.0.2"
s.platform = Gem::Platform::RUBY
s.author = "Rodrigo Navarro"
s.files = ["annotations.rb"]
s.require_path = "."
end
class Annotation
Thread.current[:current_annotations] = []
def self.+@
+new
end
def +@
Thread.current[:current_annotations] << self
end
def self.current_annotations
Thread.current[:current_annotations].pop Thread.current[:current_annotations].size
end
end
module Annotations
def method_added(name)
super
annotations = Annotation.current_annotations
return if annotations.empty?
visibility = if protected_method_defined? name
:protected
elsif private_method_defined? name
:private
else
:public
end
annotations.each do |annotation|
original_method = instance_method(name)
define_method(name) do |*args, &block|
super_method = original_method.bind(self)
annotation.call(super_method, *args, &block)
end
end
case visibility
when :protected
protected name
when :private
private name
end
end
def singleton_method_added(name)
super
annotations = Annotation.current_annotations
return if annotations.empty?
annotations.each do |annotation|
original_method = method(name)
define_singleton_method(name) do |*args, &block|
annotation.call(original_method, *args, &block)
end
end
end
end
class Memoize < Annotation
def initialize
@cache ||= {}
end
def call(method, *args, &block)
current_cache = @cache[method.receiver] ||= {}
return current_cache[args] if current_cache.has_key? args
current_cache[args] = method.call(*args, &block)
end
end
class Retry < Annotation
def initialize(times = 3)
@times = 3
end
def call(method, *args, &block)
tries = 0
begin
tries += 1
method.call(*args, &block)
rescue
retry if tries < @times
raise
end
end
end
class Lazy < Annotation
class Proxy < BasicObject
def initialize(&block)
@block = block
end
def method_missing(name, *args, &block)
@result ||= @block.call
@result.send name, *args, &block
end
end
def call(method, *args, &block)
Proxy.new { method.call(*args, &block) }
end
end
class Deprecation < Annotation
def initialize(new_name)
@new_name = new_name
end
def call(method, *args, &block)
warn "Method `#{method.name}` is deprecated. Use `#{@new_name}` instead."
method.call(*args, &block)
end
end
@juanplopes
Copy link

Só mencionando o @pedroteixeira, com quem falei sobre isso recentemente.

@UnquietCode
Copy link

This is an excellent implementation of Ruby annotations! Any interest in moving this project to GitHub as a full repository?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment