如何在Ruby中“伪造”C#​​样式属性?

时间:2009-07-05 23:30:35

标签: ruby attributes

编辑:我略微更改了规范,以便更好地匹配我想象的事情。

好吧,我真的不想伪造C#属性,我想要一对一并支持AOP。

鉴于该计划:

class Object
  def Object.profile 
    # magic code here
  end
end 

class Foo
  # This is the fake attribute, it profiles a single method.
  profile
  def bar(b)
   puts b
  end

  def barbar(b)
    puts(b)
  end

  comment("this really should be fixed")
  def snafu(b)
  end

end

Foo.new.bar("test")
Foo.new.barbar("test") 
puts Foo.get_comment(:snafu)

期望的输出:

Foo.bar was called with param: b = "test"
test
Foo.bar call finished, duration was 1ms
test
This really should be fixed

有没有办法实现这个目标?

2 个答案:

答案 0 :(得分:11)

我有一个不同的方法:

class Object
  def self.profile(method_name)
    return_value = nil
    time = Benchmark.measure do
      return_value = yield
    end

    puts "#{method_name} finished in #{time.real}"
    return_value
  end
end

require "benchmark"

module Profiler
  def method_added(name)
    profile_method(name) if @method_profiled
    super
  end

  def profile_method(method_name)
    @method_profiled = nil
    alias_method "unprofiled_#{method_name}", method_name
    class_eval <<-ruby_eval
      def #{method_name}(*args, &blk)
        name = "\#{self.class}##{method_name}"
        msg = "\#{name} was called with \#{args.inspect}"
        msg << " and a block" if block_given?
        puts msg

        Object.profile(name) { unprofiled_#{method_name}(*args, &blk) }
      end
    ruby_eval
  end

  def profile
    @method_profiled = true
  end
end

module Comment
  def method_added(name)
    comment_method(name) if @method_commented
    super
  end

  def comment_method(method_name)
    comment = @method_commented
    @method_commented = nil
    alias_method "uncommented_#{method_name}", method_name
    class_eval <<-ruby_eval
      def #{method_name}(*args, &blk)
        puts #{comment.inspect}
        uncommented_#{method_name}(*args, &blk)
      end
    ruby_eval
  end

  def comment(text)
    @method_commented = text
  end
end

class Foo
  extend Profiler
  extend Comment

  # This is the fake attribute, it profiles a single method.
  profile
  def bar(b)
   puts b
  end

  def barbar(b)
    puts(b)
  end

  comment("this really should be fixed")
  def snafu(b)
  end
end

关于这个解决方案的几点:

  • 我通过模块提供了其他方法,可以根据需要扩展到新类。这样可以避免污染所有模块的全局命名空间。
  • 我避免使用alias_method,因为模块包含允许AOP样式的扩展(在本例中为method_added),而不需要别名。
  • 我选择使用class_eval而不是define_method来定义新方法,以便能够支持带有块的方法。这也需要使用alias_method
  • 因为我选择支持块,所以在方法占用块的情况下,我还在输出中添加了一些文本。
  • 有一些方法可以获得实际参数名称,这些名称更接近原始输出,但它们并不适合这里的响应。您可以查看merb-action-args,其中我们编写了一些需要获取实际参数名称的代码。它适用于JRuby,Ruby 1.8.x,Ruby 1.9.1(带有gem)和Ruby 1.9 trunk(本机)。
  • 这里的基本技术是在调用profilecomment时存储类实例变量,然后在添加方法时应用该变量。与前面的解决方案一样,method_added挂钩用于跟踪添加新方法的时间,但不是每次都删除挂钩,而是钩子检查实例变量。应用AOP后将删除实例变量,因此它仅应用一次。如果多次使用同样的技术,可以进一步抽象。
  • 一般来说,我试图尽可能贴近你的“规范”,这就是为什么我包含了Object.profile片段而不是内联实现它。

答案 1 :(得分:7)

好问题。这是我对实现的快速尝试(我没有尝试优化代码)。我冒昧地将profile方法添加到了 Module课程。通过这种方式,它将在每个类和模块定义中可用。它会更好 将其提取到模块中,并在需要时将其混合到类Module中。

我也不知道是否要使profile方法的行为与Ruby的public / protected / private关键字相似, 但无论如何我都是这样实现的。调用profile后定义的所有方法都会被分析,直到调用noprofile

class Module
  def profile
    require "benchmark"
    @profiled_methods ||= []
    class << self
      # Save any original method_added callback.
      alias_method :__unprofiling_method_added, :method_added
      # Create new callback.
      def method_added(method)
        # Possible infinite loop if we do not check if we already replaced this method.
        unless @profiled_methods.include?(method)
          @profiled_methods << method
          unbound_method = instance_method(method)
          define_method(method) do |*args|
            puts "#{self.class}##{method} was called with params #{args.join(", ")}"
            bench = Benchmark.measure do
              unbound_method.bind(self).call(*args)
            end
            puts "#{self.class}##{method} finished in %.5fs" % bench.real
          end
          # Call the original callback too.
          __unprofiling_method_added(method)
        end
      end
    end
  end

  def noprofile # What's the opposite of profile?
    class << self
      # Remove profiling callback and restore previous one.
      alias_method :method_added, :__unprofiling_method_added
    end
  end
end

您现在可以按如下方式使用它:

class Foo
  def self.method_added(method) # This still works.
    puts "Method '#{method}' has been added to '#{self}'."
  end

  profile

  def foo(arg1, arg2, arg3 = nil)
    puts "> body of foo"
    sleep 1
  end

  def bar(arg)
    puts "> body of bar"
  end

  noprofile

  def baz(arg)
    puts "> body of baz"
  end
end

按正常方式调用方法:

foo = Foo.new
foo.foo(1, 2, 3)
foo.bar(2)
foo.baz(3)

获得基准测试输出(以及原始method_added回调的结果只是为了表明它仍然有效):

Method 'foo' has been added to 'Foo'.
Method 'bar' has been added to 'Foo'.
Method 'baz' has been added to 'Foo'.
Foo#foo was called with params 1, 2, 3
> body of foo
Foo#foo finished in 1.00018s
Foo#bar was called with params 2
> body of bar
Foo#bar finished in 0.00016s
> body of baz

需要注意的一点是,使用Ruby元编程动态获取参数名称是不可能的。 你必须解析原始的Ruby文件,这肯定是可能的,但有点复杂。查看parse_tree and ruby_parser 宝石了解详情。

有趣的改进是能够使用Module类中的类方法定义此类行为。能够做类似的事情会很酷:

class Module
  method_wrapper :profile do |*arguments|
    # Do something before calling method.
    yield *arguments  # Call original method.
    # Do something afterwards.
  end
end

我将把这个元元编程练习再次留下。 : - )