如何在Ruby中封装包含的模块方法?

6

我希望能够在模块中定义一些方法,但这些方法不能被包含该模块的类所访问。以下是一个示例:

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

我希望Foo.new.do_stuff会因为试图访问模块试图隐藏的方法而崩溃。然而,在上面的代码中,Foo.new.do_stuff将工作正常 :(
在Ruby中有实现我想要做的事情的方法吗?
更新-真正的代码
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

显然,这段代码已经被粗略地编写出来并需要进行严肃的重构。我的问题是,是否有一种方法可以在不意外覆盖ActiveRecord :: Base上的某个方法或任何其他包含在Place.rb中的模块的情况下,对此模块进行重构。


关于第一个例子 - Bar#do_stuff 本质上是对 common_method_name 的公共接口,因此代码不应该出现任何逻辑错误。只有在执行 Foo.new.common_method_name 时才会出现错误。 - Alexander Popov
3个回答

5
我不相信有任何简单的方法可以做到这一点,这是有意设计的。如果您需要行为封装,您可能需要类,而不是模块。
在Ruby中,私有方法和公共方法之间的主要区别在于私有方法只能在没有显式接收者的情况下调用。调用MyObject.new.my_private_method将导致错误,但在MyObject的方法定义中调用my_private_method将正常工作。
当您将一个模块混合到一个类中时,该模块的方法被“复制”到该类中:

如果我们在类定义中包含一个模块,它的方法就会被有效地附加或“混合”到类中。- Ruby用户指南

就类而言,该模块不再存在于外部实体中(但请参见Marc Talbot的下面的评论)。您可以从类中调用模块的任何方法,而无需指定接收者,因此它们实际上不再是模块的“私有”方法,而是类的私有方法。

1
严格来说,这并不完全正确。该模块确实作为一个单独的实体存在,尽管它在类的继承链中。类本身对于模块中的方法一无所知 - 当在类上进行模块方法调用时,类基本上会说“我不知道如何做到这一点,也许我的某个基类知道”,然后将其抛到链中(其中混合了该模块)。 - Marc Talbot
@MarcTalbot 谢谢。我修改了我的答案。 - Brandan

1
这是一个相当古老的问题,但我感到有必要回答它,因为接受的答案缺少 Ruby 的一个关键特性。
该特性称为模块构建器,以下是定义模块以实现它的方法:
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

现在你可以使用以下代码来引入这个模块:

class Foo
  include RecursiveTreeQueries.new
end

你需要在这里实例化模块,因为RecursiveTreeQueries不是一个模块本身,而是一个类(Module类的子类)。你可以进一步重构以减少方法之间的大量重复,我只是拿了你的代码来演示概念。


非常好的答案!非常感谢! - Chris Aitchison

0

当模块被包含时,请将方法标记为私有。

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

2
这并不会阻止包含类调用 common_method_name。它只是将该方法标记为私有,这在 OP 的代码中已经是这样的情况了。 - Brandan
是的,你完全正确...我想我有点误读了问题。在这种情况下,整个目的对我来说有点令人困惑,我不确定该怎么做。 :( - Veraticus
1
目的是在模块中实现辅助方法的封装,以便包含具有相同私有方法名称的两个或多个模块不会引入任何奇怪的行为。 - Chris Aitchison
1
我想我只是希望能够在模块内重构方法,而不必担心(通过重构)创建的模块私有方法泄漏。 - Chris Aitchison

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接