2009-07-05 31 views
5

EDIT: Cambié ligeramente las especificaciones, para que coincida con lo que imaginaba que hacer.¿Cómo puedo "falsificar" los atributos de estilo C# en Ruby?

Bueno, realmente no quiero falsificar los atributos de C#, quiero ponerlos encima y apoyar el AOP también.

Teniendo en cuenta el programa:

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) 

salida deseada:

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

¿Hay alguna manera de lograr esto?

Respuesta

11

Tengo un enfoque algo diferente:

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 

algunos puntos sobre esta solución:

  • I proporcionan los métodos adicionales a través de módulos que podrían extenderse en nuevas clases, según sea necesario . Esto evita contaminar el espacio de nombre global para todos los módulos.
  • Evité usar alias_method, ya que el módulo incluye permitir extensiones de estilo AOP (en este caso, para method_added) sin la necesidad de aliasing.
  • Elegí usar class_eval en lugar de define_method para definir el nuevo método para poder admitir métodos que toman bloques. Esto también requirió el uso de alias_method.
  • Como elegí para admitir bloques, también agregué un poco de texto a la salida en caso de que el método tome un bloque.
  • Hay formas de obtener los nombres de los parámetros reales, que estarían más cerca de su salida original, pero aquí no caben realmente en una respuesta. Puede consultar merb-action-args, donde escribimos algunos códigos que requerían obtener los nombres de los parámetros reales. Funciona en JRuby, Ruby 1.8.x, Ruby 1.9.1 (con una gema) y Ruby 1.9 trunk (de forma nativa).
  • La técnica básica aquí es almacenar una variable de instancia de clase cuando se llama a profile o comment, que luego se aplica cuando se agrega un método. Como en la solución anterior, el gancho method_added se usa para rastrear cuando se agrega el nuevo método, pero en vez de eliminar el gancho cada vez, el gancho busca una variable de instancia. La variable de instancia se elimina después de aplicar el AOP, por lo que solo se aplica una vez. Si esta misma técnica se usó varias veces, podría resumirse más.
  • En general, traté de permanecer lo más cerca posible de su "especificación", por lo que incluí el fragmento Object.profile en lugar de implementarlo en línea.
+0

+1 respuesta fantástica. –

7

Gran pregunta. Este es mi intento rápido de implementación (no intenté optimizar el código). Me tomé la libertad de agregar el método profile a la clase Module. De esta manera estará disponible en cada clase y definición de módulo. Sería incluso mejor extraerlo en un módulo y mezclarlo en la clase Module siempre que lo necesite.

Yo tampoco sabía si el punto era hacer que el método profile se comportan como public/protected/private palabras clave de Ruby, pero me puso en práctica el estilo de todos modos. Todos los métodos definidos después de llamar a profile son perfilados, hasta que se llama al 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 

Ahora puede utilizar de la siguiente manera:

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 

llamar a los métodos como lo haría normalmente:

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

Y obtener una salida como punto de referencia (y el resultado del original method_added de devolución de llamada solo para mostrar que todavía funciona):

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 

Una cosa a tener en cuenta es que es imposible obtener dinámicamente el nombre de los argumentos con la meta-programación de Ruby. Tendría que analizar el archivo Ruby original, que sin duda es posible pero un poco más complejo. Consulte las gemas parse_tree and ruby_parser para obtener más detalles.

Una mejora divertida sería poder definir este tipo de comportamiento con un método de clase en la clase Module. Sería genial ser capaz de hacer algo como:

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

voy a dejar este ejercicio meta-meta-programación para otro momento. :-)

+0

+1 respuesta alucinante, agregará algunos comentarios en mi pregunta –

+0

Nota: voy a esperar 24 horas más o menos antes de aceptar esto, para alentar a algunos más comentarios sobre la cuestión. Agregué especificaciones extendidas, por lo que es mejor que coincida con la forma en que C# attribs. Gracias de nuevo por la respuesta detallada y bien pensada –

+0

Ruby 1.9.2 tiene ProC# args, por lo que * puede * realmente obtener los nombres de los parámetros. –

Cuestiones relacionadas