Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby: Mixin which adds dynamic instance methods whose names are created using a class method

I have the following:

module Thing
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    attr_reader :things

    def has_things(*args)
      options = args.extract_options! # Ruby on Rails: pops the last arg if it's a Hash

      # Get a list of the things (Symbols only)
      @things = args.select { |p| p.is_a?(Symbol) }

      include InstanceMethods
    end
  end

  module InstanceMethods
    self.class.things.each do |thing_name| # !!! Problem is here, explaination below
      define_method "foo_for_thing_#{thing_name}" do
        "bar for thing #{thing_name}"
      end
    end
  end
end

In another class which mixes-in the Thing module:

class Group
  has_things :one, :two, :option => "something"
end

When calling has_things within a class, I would like to have the dynamic "foo_for_thing_one" and "foo_for_thing_two" instance methods available. For example:

@group = Group.new
@group.foo_for_thing_one # => "bar for thing one"
@group.foo_for_thing_two # => "bar for thing two"

However, I get the following error:

`<module:InstanceMethods>': undefined method `things' for Module:Class (NoMethodError)

I realize that "self" in the problem line pointed out above (first line of the InstanceMethods module) refers to the InstanceMethods module.

How do I reference the "things" class method (which returns [:one, :two] in this example) so I can loop through and create dynamic instance methods for each? Thanks. Or if you have other suggestions for accomplishing this, please let me know.

like image 435
robertwbradford Avatar asked Apr 06 '11 21:04

robertwbradford


1 Answers

Quick answer:

Put the contents of InstanceMethods inside the has_things method definition and remove the InstanceMethods module.

Better answer:

Your use of the InstanceMethods-ClassMethods anti-pattern is especially unwarranted here and cargo-culting it has added to your confusion about scope and context. Do the simplest thing that could possibly work. Don't copy someone else's code without critical thinking.

The only module you need is ClassMethods, which should be given a useful name and should not be included but rather used to extend the class that you want to grant the has_things functionality. Here's the simplest thing that could possibly work:

module HasThings
  def has_things(*args)
    args.each do |thing|
      define_method "thing_#{thing}" do
        "this is thing #{thing}"
      end
    end
  end
end

class ThingWithThings
  extend HasThings
  has_things :foo
end

ThingWithThings.new.thing_foo # => "this is thing foo"

Only add complexity (options extraction, input normalization, etc) when you need it. Code just in time, not just in case.

like image 59
Rein Henrichs Avatar answered Nov 09 '22 03:11

Rein Henrichs