ashr81
ashr81

Reputation: 650

Include a module on instances instead of a class

I have a module:

module MM::NN
  def test_method
    puts "module method called"
  end
end

I am trying to include this module in a class A per instance depending on a parameter passed to the initializer of the instance:

class A
  def initialize(add_module)
    self.class.send(:include, MM::NN) if add_module
  end
end

I expected:

A.new(true).test_method  # >> module method called
A.new(false).test_method  # >> NoMethodError

But test_method is defined on all instances. Calling the class with argument true adds the module to the instances created later as well. I got:

A.new(true).test_method  # >> module method called
A.new(false).test_method  # >> NoMethodError

Initializing the class with false argument on first initializer call and true in the second will give the desired result, as test_method method is added later:

A.new(false).test_method  # >> NoMethodError
A.new(true).test_method  # >> module method called

because the module is attached to the class itself.

How can I make the method available on specific cases like above?

How do i solve an another case:
where class A inherit properties and methods of another class B and class B as well contains test_method. How is it possible to use the module TestModules test_method instead of method inside the class B.

And what are the differences(or)side-effects in calling methods by including a module inside a class: 1) include module outside initialize.
2) Include module inside initialize.

Upvotes: 3

Views: 1015

Answers (4)

max pleaner
max pleaner

Reputation: 26768

When you call self.class.send(:include, MM::NN), that's making a irreversible change to the class (the blueprint for all instances).

If you want a module to only be included for an instance, you have to use the singleton class (What exactly is the singleton class in ruby?):

module TestModule
  def test_method; "ok"; end
end

class A
    def initialize(add_module)
        singleton_class.include(TestModule) if add_module
    end
end

A.new(true).test_method  # module method called
A.new(false).test_method # NoMethodError

Note that I changed display to test_method, because display is a built in Kernel method so it won't raise NoMethodError even if you don't include the module on the instance.

Note, Ozer's answer is the same thing but using a different syntax.

-- edit in response to comment --

Adding methods on the singleton class takes priority over those added to the regular class, and if you include multiple modules onto the singleton class, the last on will take priority:

module A; def fn; 1; end; end
module B; def fn; 2; end; end
module C; def fn; 3; end; end

class D
  include A
  def initialize(add_b, add_c)
    singleton_class.include(B) if add_b
    singleton_class.include(C) if add_c
  end
end

puts D.new(false, false).fn # => 1
puts D.new(true, true).fn   # => 3

Upvotes: 7

matt
matt

Reputation: 79733

include adds all the methods from the module to any instances of the class that includes the module. To add the methods to an individual object you can use extend:

class A
  def initialize(add_module)
    extend(MM::NN) if add_module
  end
end

Upvotes: 5

Andrew Schwartz
Andrew Schwartz

Reputation: 4657

You could add it to the singleton class as others suggest above. But I would suggest that the fact that OOP constructs are making your task hard is because you are approaching the problem from a bad angle. I think having instances of class A behave so differently by having different methods and ancestors is going to cause a lot more confusion than it solves.

What I'd suggest instead is that you try to accomplish what you are doing using composition instead of inheritance. That is, have an instance variable inside class A that contains the behavior in theMM:NN. This depends on the specifics of the behavior in that module, but could roughly look like this:

# This class is here because I'm assuming you can't make `MM::NN` a class in its own right. However it probably needs additional data or methods to actually work with the MM::NN methods
class NN_implementation
  include MM::NN
end

class A
  def initialize(use_module)
    # if this class needs access to data or methods from A, you could add `self` as an input arg to the constructor of `NN_implementation`
    @nn = NN_implmenetation.new if use_module
  end

  def has_nn?
    [email protected]?
  end
end

Then you can either (1) expose the specific NN methods you want as methods on class A, or you can expose the nn instance var using attr_reader and call something like my_a_instnace.nn.some_nn_method if my_a_instance.has_nn?. Again, largely depends on the specifics of A and MM::NN how I would ultimately code this up for prod-ready code, but something along these lines keeps class definitions clean and makes it not confusing where specific behavior comes from.

Upvotes: 1

Ozer Chagatai
Ozer Chagatai

Reputation: 31

This will add it to just the instance that is being initialized:

class A
  def initialize(add_module)
    if add_module
      (class <<self; include MM::NN; end)
    end
  end
end

Upvotes: 2

Related Questions