进入Ruby元编程:为多个内部方法生成代理方法

时间:2010-06-14 19:27:35

标签: ruby metaprogramming

我已经多次听到Ruby吹捧其超级壮观的元编程功能,我想知道是否有人可以帮助我开始解决这个问题。

我有一个类作为各种“归档”,使用内部方法根据输入处理和输出数据。但是,出于性能目的,类本身中存档中的项目将使用整数进行表示和处理。存档之外的实际项目以字符串表示形式表示,它只是number_representation.to_s(36)。

因此,我使用“代理方法”连接每个内部方法,该方法将输入转换为归档识别的整数形式,运行内部方法,并转换输出(单个其他项目,或者他们的集合)回到字符串。

命名约定是这样的:内部方法由_method_name表示;它们对应的代理方法由method_name表示,没有前导下划线。

例如:

class Archive

  ## PROXY METHODS ##
  ## input: string representation of id's
  ## output: string representation of id's

  def do_something_with id
    result = _do_something_with id.to_i(36)
    return nil if result == nil
    return result.to_s(36)
  end

  def do_something_with_pair id_1,id_2
    result = _do_something_with_pair id_1.to_i(36), id_2.to_i(36)
    return nil if result == nil
    return result.to_s(36)
  end

  def do_something_with_these ids
    result = _do_something_with_these ids.map { |n| n.to_i(36) }
    return nil if result == nil
    return result.to_s(36)
  end

  def get_many_from id
    result = _get_many_from id
    return nil if result == nil         # no sparse arrays returned
    return result.map { |n| n.to_s(36) }
  end

  ## INTERNAL METHODS ##
  ## input: integer representation of id's
  ## output: integer representation of id's

  private

  def _do_something_with id
    # does something with one integer-represented id,
    # returning an id represented as an integer
  end

  def do_something_with_pair id_1,id_2
    # does something with two integer-represented id's,
    # returning an id represented as an integer
  end

  def _do_something_with_these ids
    # does something with multiple integer ids,
    # returning an id represented as an integer
  end

  def _get_many_from id
    # does something with one integer-represented id,
    # returns a collection of id's represented as integers
  end
end

如果在内部方法的开头有id.class == String,我就不能只转换它们有几个原因:

  1. 这些内部方法是计算密集型的递归函数,我不希望每一步都多次检查的开销
  2. 在没有添加额外参数的情况下,无法判断是否在最后重新转换
  3. 我想将此视为理解ruby元编程的练习
  4. 有没有人有任何想法?


    修改

    我想要的解决方案最好能够采用一系列方法名称

    @@PROXY_METHODS = [:do_something_with, :do_something_with_pair,
                       :do_something_with_these, :get_many_from]
    

    遍历它们,并在每次迭代中,推出代理方法。我不确定如何处理这些参数,但有没有办法测试方法的参数?如果没有,那么简单的鸭子打字/类似概念也可以。


    我使用#class_eval

    提出了我自己的解决方案
    @@PROXY_METHODS.each do |proxy|
      class_eval %{ def #{proxy} *args
                      args.map! do |a|
                        if a.class == String
                          a.to_i(36)
                        else
                          a.map { |id| id.to_i(36) }
                        end
                      end
                      result = _#{proxy}(*args)
    
                      result and if result.respond_to?(:each)
                                   result.map { |r| r.to_s(36) }
                                 else
                                   result.to_s(36)
                                 end
                    end
                  }
    end
    

    然而,#class_eval似乎有点......凌乱?与“应该”相比,还是不优雅的。

1 个答案:

答案 0 :(得分:2)

class Archive
  # define a new method-creating method for Archive by opening the
  # singleton class for Archive
  class << Archive
    private # (make it private so no one can call Archive.def_api_method)
    def def_api_method name, &defn
      define_method(name) do |*args|
        # map the arguments to their integer equivalents,
        # and pass them to the method definition
        res = defn[ *args.map { |a| a.to_i(36) } ]
        # if we got back a non-nil response, 
        res and if res.respond_to?(:each)
                  # map all of the results if many returned
                  res.map { |r| r.to_s(36) } 
                else
                  # map the only result if only one returned
                  res.to_s(36)
                end
      end
    end
  end
  def_api_method("do_something_with"){ |id| _do_something_with(id) }
  def_api_method("do_something_with_pair"){ |id_1, id_2| _do_something_with_pair id_1.to_i(36), id_2.to_i(36) }
  #...
end

您可以使用

来定义它,而不是打开单例来定义Archive.def_api_method
class Archive
  def Archive.def_api_method
    #...

但我没有这样做的原因是任何有权访问Archive类的人都可以使用Archive.def_api_method调用它。打开单例类允许我将def_api_method标记为私有,因此只能在self == Archive时调用它。

如果您总是要调用具有相同(或可派生)名称的内部版本,那么您可以使用#send直接调用它(而不是传递定义块)。

class Archive
  # define a method-creating method that wraps an internal method for external use
  class << Archive
    private # (make it private so no one can call Archive.api_method)
    def api_method private_name
      public_name = private_name.to_s.sub(/^_/,'').to_sym
      define_method(public_name) do |*args|
        # map the arguments to their integer equivalents,
        # and pass them to the private method
        res = self.send(private_name, *args.map { |a| a.to_i(36) })
        # if we got back a non-nil response, 
        res and if res.respond_to?(:each)
                  # map all of the results if many returned
                  res.map { |r| r.to_s(36) } 
                else
                  # map the only result if only one returned
                  res.to_s(36)
                end          end
      # make sure the public method is publicly available
      public public_name
    end
  end

  api_method :_do_something_with
  api_method :_do_something_with_pair

  private

  def _do_something_with
    #...
  end
  def _do_something_with_pair
    #...
  end
end

这更类似于其他元方法,例如attr_readerattr_writer