6

I want to be able to have methods in a module that are not accessible by the class that includes the module. Given the following example:

class Foo
  include Bar

  def do_stuff
    common_method_name
  end
end

module Bar
  def do_stuff
    common_method_name
  end

  private
  def common_method_name
    #blah blah
  end
end

I want Foo.new.do_stuff to blow up because it is trying to access a method that the module is trying to hide from it. In the code above, though, Foo.new.do_stuff will work fine :(

Is there a way to achieve what I want to do in Ruby?

UPDATE - The real code

class Place < ActiveRecord::Base
  include RecursiveTreeQueries

  belongs_to :parent, {:class_name => "Place"}
  has_many :children, {:class_name => 'Place', :foreign_key => "parent_id"}
end


module RecursiveTreeQueries

  def self_and_descendants
     model_table = self.class.arel_table
     temp_table = Arel::Table.new :temp
     r = Arel::SelectManager.new(self.class.arel_engine).from(model_table).project(model_table.columns).join(temp_table).on('true').where(model_table[:parent_id].eq(temp_table[:id]))
     nr = Place.scoped.where(:id => id)
     q = Arel::SelectManager.new(self.class.arel_engine)
     as = Arel::Nodes::As.new temp_table, nr.union(r)
     arel = Arel::SelectManager.new(self.class.arel_engine).with(:recursive,as).from(temp_table).project(temp_table[:id])
     self.class.where(model_table[:id].in(arel))
   end  

  def self_and_ascendants
    model_table = self.class.arel_table
    temp_table = Arel::Table.new :temp
    r = Arel::SelectManager.new(self.class.arel_engine).from(model_table).project(model_table.columns).join(temp_table).on('true').where(temp_table[:parent_id].eq(model_table[:id]))
    nr = Place.scoped.where(:id => id)
    q = Arel::SelectManager.new(self.class.arel_engine)
    as = Arel::Nodes::As.new temp_table, nr.union(r)
    arel = Arel::SelectManager.new(self.class.arel_engine).with(:recursive,as).from(temp_table).project(temp_table[:id])
    self.class.where(model_table[:id].in(arel))
 end

end

Clearly this code is hacked out and due some serious refactoring, and the purpose of my question is to find out if there is a way I can refactor this module with impunity from accidentally overwriting some method on ActiveRecord::Base or any other module included in Place.rb.

Chris Aitchison
  • 4,656
  • 1
  • 27
  • 43
  • As for the first example - `Bar#do_stuff` is essentially a public interface to `common_method_name`, so there is no logic that the code should break. It should break in case you do `Foo.new.common_method_name`. – Alexander Popov Jun 06 '14 at 17:41

3 Answers3

5

I don't believe there's any straightforward way to do this, and that's by design. If you need encapsulation of behavior, you probably need classes, not modules.

In Ruby, the primary distinction between private and public methods is that private methods can only be called without an explicit receiver. Calling MyObject.new.my_private_method will result in an error, but calling my_private_method within a method definition in MyObject will work fine.

When you mix a module into a class, the methods of that module are "copied" into the class:

[I]f we include a module in a class definition, its methods are effectively appended, or "mixed in", to the class. — Ruby User's Guide

As far as the class is concerned, the module ceases to exist as an external entity (but see Marc Talbot's comment below). You can call any of the module's methods from within the class without specifying a receiver, so they're effectively no longer "private" methods of the module, only private methods of the class.

Brandan
  • 14,735
  • 3
  • 56
  • 71
  • 1
    Strictly speaking, that isn't quite true. The module DOES exist as a separate entity, albeit one that is in the inheritance chain of the class. The class itself knows nothing about the methods that are in the module - when a module method call is made on a class, the class basically says "I have no idea how to do that, maybe one of my base classes does" and tosses it up the chain (into which the module is mixed). – Marc Talbot Feb 22 '12 at 02:27
  • @MarcTalbot Thank you. I amended my answer. – Brandan Feb 22 '12 at 02:47
1

This is quite an old question, but I feel compelled to answer it since the accepted answer is missing a key feature of Ruby.

The feature is called Module Builders, and here is how you would define the module to achieve it:

class RecursiveTreeQueries < Module
  def included(model_class)
    model_table = model_class.arel_table
    temp_table = Arel::Table.new :temp
    nr = Place.scoped.where(:id => id)
    q = Arel::SelectManager.new(model_class.arel_engine)
    arel_engine = model_class.arel_engine

    define_method :self_and_descendants do
      r = Arel::SelectManager.new(arel_engine).from(model_table).project(model_table.columns).join(temp_table).on('true').where(model_table[:parent_id].eq(temp_table[:id]))
      as = Arel::Nodes::As.new temp_table, nr.union(r)
      arel = Arel::SelectManager.new(arel_engine).with(:recursive,as).from(temp_table).project(temp_table[:id])
      self.class.where(model_table[:id].in(arel))
    end

    define_method :self_and_ascendants do
      r = Arel::SelectManager.new(arel_engine).from(model_table).project(model_table.columns).join(temp_table).on('true').where(temp_table[:parent_id].eq(model_table[:id]))
      as = Arel::Nodes::As.new temp_table, nr.union(r)
      arel = Arel::SelectManager.new(arel_engine).with(:recursive,as).from(temp_table).project(temp_table[:id])
      self.class.where(model_table[:id].in(arel))
    end
  end
end

Now you can include the module with:

class Foo
  include RecursiveTreeQueries.new
end

You need to actually instantiate the module here since RecursiveTreeQueries is not a module itself but a class (a subclass of the Module class). You could refactor this further to reduce a lot of duplication between methods, I just took what you had to demonstrate the concept.

Chris Salzberg
  • 27,099
  • 4
  • 75
  • 82
0

Mark the method private when the module is included.

module Bar
  def do_stuff
    common_method_name
  end

  def common_method_name
    #blah blah
  end

  def self.included(klass)
      klass.send(:private, :common_method_name)
  end
end
Veraticus
  • 15,944
  • 3
  • 41
  • 45
  • 2
    This does not prevent an including class from calling `common_method_name`. It just marks that method as private, which is already the case in the OP's code. – Brandan Feb 22 '12 at 02:33
  • Yeah, you're totally right... I guess I misread the question a little bit. In this case the whole purpose behind it is a little confusing to me then, and I'm not sure how to do it. :( – Veraticus Feb 22 '12 at 02:35
  • 1
    The purpose is to enable the encapsulation of helper methods in the module. So that including two or more modules with the same private method names doesn't introduce any weird behaviour. – Chris Aitchison Feb 22 '12 at 05:50
  • 1
    I guess I just want to be able to refactor methods within a module without worrying about the private methods of the module (that I create with the refactoring) leaking. – Chris Aitchison Feb 22 '12 at 05:52