Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I encapsulate included module methods in Ruby?

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.

like image 612
Chris Aitchison Avatar asked Feb 22 '12 01:02

Chris Aitchison


2 Answers

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.

like image 175
Brandan Avatar answered Oct 02 '22 16:10

Brandan


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.

like image 31
Chris Salzberg Avatar answered Oct 02 '22 16:10

Chris Salzberg