Sonntag, 23. November 2014

Modularize ActiveRecord has_many proxy extensions!

There are several reasons for extending ActiveRecord::Associations::ClassMethods#has_many associations (please read Extend has_many association proxy!).
One step further in modularization terms is to put those extension methods into a separate module, because it is a matter of:
  1. reuseability (logic can be reused)
  2. concern (logic is consolidated in a concerning module)
Please note, that moving the methods into a concerning module also means moving it out of the first view. But when it comes to reuseability, there is no alternative.
Imagine the example:
class Recipe < ActiveRecord::Base
  def to_s
    "#{name} (#{time})"
  end
end

class Cook < ActiveRecord::Base
  has_many :recipes do
    def to_s
      map(&:to_s).join(', ')
    end

    def [] term
      where("#{proxy_association.klass.table_name}.name LIKE ?", "%#{term}%")
    end
  end
end
and a use case like:
Cook.first.recipes['cookie'].to_s
=> "Chocolate Chunk Cookies (30), Amish Cookies (60)"
can be refactored by moving both Proxy extension methods into a module:
module HasManyProxyExtension
  module NameSearchableAndHumanizable
     def [] term
      where("#{proxy_association.klass.table_name}.name LIKE ?", "%#{term}%")
    end

    def to_s
      map(&:to_s).join(', ')
    end
  end
end

class Cook < ActiveRecord::Base
  has_many :recipes, 
    extend: HasManyProxyExtension::NameSearchableAndHumanizable
end
and the use case again:
Cook.first.recipes['cookie'].to_s
=> "Chocolate Chunk Cookies (30), Amish Cookies (60)"
is still working with the advantage being able to reuse the module in another class:
class Recipe < ActiveRecord::Base
  has_many :ingredients, 
    extend: HasManyProxyExtension::NameSearchableAndHumanizable
  def to_s
    "#{name} (#{time})"
  end
end

class Ingredient < ActiveRecord::Base
  def to_s
    return name if legal?
    "#{name} (illegal)"
  end
end
and its use case:
Recipe.first.ingredients['as'].to_s
=> "Basil, Sassafras (illegal)"
Imagine, how the model classes would look like, if the logic could not be moved into a module.
Some times it is good choice to extend an association proxy with multiple modules:
module HasManyProxyExtension
  module NameSearchable
     def [] term
      where("#{proxy_association.klass.table_name}.name LIKE ?", "%#{term}%")
    end
  end

  module Humanizable
    def to_s
      map(&:to_s).join(', ')
    end
  end
end

class Cook < ActiveRecord::Base
  has_many :recipes, 
    extend: [HasManyProxyExtension::NameSearchable, HasManyProxyExtension::Humanizable]
end
Of course it still works the same. But It allows deeper modularization and a more versatile combination of modules.
Further articles of interest:

Supported by Ruby 2.1.1 and Ruby on Rails 3.2.17