Another first four weeks: concerned and delegated

November 10, 2011 Stephan Hagemann

James’ post from a couple of weeks ago inspired me to write up my own experiences of my first couple of weeks at Pivotal. However, instead of telling you how it felt, I will tell you about stuff I learned.

ActiveSupport::Concern

I have seen and used this pattern of adding both class and instance methods from a module to a class:

module Foo
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def foo
      'classy'
    end
  end

  def foo
    'instancy'
  end
end

class Bar
  include Foo
end

Bar.foo # => "classy"
Bar.new.foo # => "instancy"

While the syntax for including Foo in Bar is nice and succinct, the definition of Foo contains the funny looking “included” hook to also extend the base class.

If you happen to have ActiveSupport around, you can instead define Foo as follows while achieving the same result:

module Foo
  extend ActiveSupport::Concern

  module ClassMethods
    def foo
      'classy'
    end
  end

  def foo
    'instancy'
  end
end

The documentation explains the more convenient feature of Concern, which is module dependency. Using Concern, module dependencies are correctly loaded without the need for you to be concerned.

DelegateClass

In one of the projects I worked on, we were exporting data from a Rails app. We wanted access to the business logic of the models while not littering the application’s code with export related code. We decided to extend the models using DelegateClass (see Jeff’s blog post on DelegateClass as a starting point). And DelegateClass rocked our world, too.

A couple of things to note:

  • It turns out, that these days there is no need to manually ensure that id gets delegated. It works out of the box.
  • Class methods need manual delegation.

Here is a basic delegation example:

require 'delegate'

class Foo
  def to_s
    "foo"
  end
end

module Export
  class Foo < DelegateClass(Foo)
    def bar
      "#{self} bar"
    end
  end
end

foo = Foo.new

foo.to_s # => "foo"

Export::Foo.new(foo).to_s # => "foo"
Export::Foo.new(foo).bar # => "foo bar"

When delegating ActiveRecord models with associations between them, the following pattern can be helpful to make sure you get the delegated classes when traversing relationships:

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

module Export
  class Post < DelegateClass(Post)
    alias_method :original_comments, :comments
    def comments
      original_comments.map { |comment| Export::Comment.new(comment) }
    end

    #some methods for exporting
  end
end

module Export
  class Comment < DelegateClass(Comment)
    alias_method :original_post, :post
    def post
      Export::Post.new(original_post)
    end

    #some more methods for exporting
  end
end

original_post = Post.new(comments: [Comment.new])
original_post.comments.first.class # => Comment(...)
post = Export::Post.new(original_post)
post.comments.first.class # => Export::Comment

#Be aware!
original_post == post # => true
original_post.class == post.class # => false

About the Author

Biography

Previous
Terminal: Beyond Ctrl + A and Ctrl + E
Terminal: Beyond Ctrl + A and Ctrl + E

Update: I had some of the Ctrl and Option commands switched. Fixed now. As Rails developers, we spend most...

Next
New Tech Talk: Hadoop for Rubyists
New Tech Talk: Hadoop for Rubyists

Is your MySQL database groaning under the weight of too much data? Are you tired of waiting for analytics r...