11

Is there any clean way to initialize instance variables in a Module intended to be used as Mixin? For example, I have the following:

module Example

  def on(...)   
    @handlers ||= {} 
    # do something with @handlers
  end

  def all(...)
    @all_handlers ||= []
    # do something with @all_handlers
  end

  def unhandled(...)
    @unhandled ||= []
    # do something with unhandled
  end

  def do_something(..)
    @handlers     ||= {}
    @unhandled    ||= []
    @all_handlers ||= []

    # potentially do something with any of the 3 above
  end

end

Notice that I have to check again and again if each @member has been properly initialized in each function -- this is mildly irritating. I would much rather write:

module Example

  def initialize
    @handlers     = {}
    @unhandled    = []
    @all_handlers = []
  end

  # or
  @handlers  = {}
  @unhandled = []
  # ...
end

And not have to repeatedly make sure things are initialized correctly. However, from what I can tell this is not possible. Is there any way around this, besides adding a initialize_me method to Example and calling initialize_me from the extended Class? I did see this example, but there's no way I'm monkey-patching things into Class just to accomplish this.

John Ledbetter
  • 13,557
  • 1
  • 61
  • 80

4 Answers4

13
module Example
  def self.included(base)
    base.instance_variable_set :@example_ivar, :foo
  end
end

Edit: Note that this is setting a class instance variable. Instance variables on the instance can't be created when the module is mixed into the class, since those instances haven't been created yet. You can, though, create an initialize method in the mixin, e.g.:

module Example
  def self.included(base)
    base.class_exec do
      def initialize
        @example_ivar = :foo
      end
    end
  end
end

There may be a way to do this while calling the including class's initialize method (anybody?). Not sure. But here's an alternative:

class Foo
  include Example

  def initialize
    @foo = :bar
    after_initialize
  end
end

module Example
  def after_initialize
    @example_ivar = :foo
  end
end
Mori
  • 27,279
  • 10
  • 68
  • 73
  • Excellent, thanks -- I wonder why I didn't see this approach mentioned anywhere even though there are a million articles about it. – John Ledbetter Sep 25 '12 at 15:26
  • Unfortunately this doesn't seem to work -- referencing `@example_ivar` then comes back as `nil`. – John Ledbetter Sep 25 '12 at 15:39
  • Ah, I see, thanks -- but this will cause issues if the class already specifies an `initialize` method, right? – John Ledbetter Sep 25 '12 at 15:48
  • Thanks Mori. `after_initialize` is an `ActiveRecord` callback, right? In this case I'm not using ActiveRecord/Rails, just plain Ruby. It may be that the way I want this to work is just not supported by the language / this design pattern. – John Ledbetter Sep 25 '12 at 16:55
  • @JohnLedbetter, you can name the `after_initialize` method whatever you want. It's just a means of sending a message from `Foo` to `Example` on `Foo#initialize`. It isn't related here to `ActiveRecord`. – Mori Sep 25 '12 at 17:05
2

Perhaps this is a little hacky, but you can use prepend to get the desired behavior:

module Foo
  def initialize(*args)
    @instance_var = []
    super
  end
end

class A
  prepend Foo
end

Here is the output from the console:

2.1.1 :011 > A.new
 => #<A:0x00000101131788 @instance_var=[]>
Max
  • 15,157
  • 17
  • 82
  • 127
  • 1
    Are there any downsides to using prepend instead of include? – Olivier Lance Sep 15 '14 at 17:39
  • 1
    The one downside I can think of is that the module gets inserted below the class in the hierarchy. Normally, our type hierarchy would look like `A < Foo < Object`, but now we have `Foo < A < Object`. This means that methods in Foo override methods in A, which is opposite of what we would normally expect. – Max Sep 16 '14 at 03:06
  • all right :) Well "prepend" is quite self-explanatory about it I guess, so I like (and used) your solution, thanks! (+1'd) – Olivier Lance Sep 16 '14 at 12:23
1

modules provides hooks, as Module#included. I suggest you check out ruby doc on the topic, or use ActiveSupport::Concern, which provides some helpers on modules.

ksol
  • 11,835
  • 5
  • 37
  • 64
0

I think there may be a simpler answer to this. The module should have an initializer that initialises the variables as you normally would do. In the initializer for the class that includes the module, invoke super() to invoke the initializer in the included module. This is simply following the method dispatch rules in Ruby.

On reflection, this will not work so well if the class including the module also has a superclass that needs to be initialised. The initializer in the module would need to accept a variable parameter list and pass this up to the superclass. It looks like a good avenue to explore though.

ComDubh
  • 793
  • 4
  • 18