【问题标题】:How do I run code before and after a method in a sub class?如何在子类中的方法之前和之后运行代码?
【发布时间】:2010-07-07 18:16:40
【问题描述】:

我的第一个想法是这样的:

class AbstractBuilder
  attr_reader :time_taken

  def build_with_timer
    started_at = Time.now
    build
    @time_taken = Time.now - started_at
  end

  def build
    raise 'Implement this method in a subclass' 
  end
end

class MyBuilder < AbstractBuilder
  def build
    sleep(5)
  end
end

builder = MyBuilder.new.build_with_timer
puts builder.time_taken

我怀疑有更好的方法可以提供更好的灵活性,例如,理想情况下,我想在 MyBuilder 实例上调用“build”而不是“build_with_timer”,并始终记录执行时间。

我确实考虑过在初始化时使用 alias_method,甚至使用模块 mixin 而不是类继承,这会覆盖在中间调用 super 的构建方法(不确定这是否可行)。在我进入兔子洞之前,我想我会看看是否有既定的做法。

【问题讨论】:

  • 为了澄清我想在一个子类中定义一个已知名称的方法,该子类使用基类中定义的代码透明地包装(之前和之后)。与 ActiveRecord 过滤器相比,我需要更少的灵活性/开销。

标签: ruby metaprogramming


【解决方案1】:

我尝试了一个版本来实现你想要的。这个版本也不需要子类有任何额外的代码。

class AbstractBuilder

  @@disable_override = false

  def before_method
    puts "before"
  end

  def after_method
    puts "after"
  end

  def self.method_added name
    unless @@disable_override
      if name == :build
        @@disable_override = true # to stop the new build method 
        self.send :alias_method, :sub_build, :build
        self.send :remove_method, :build
        self.send :define_method, :build do
          before_method
          sub_build
          after_method
        end
        @@disable_override = false
      else
        puts "defining other method #{name}"
      end
    end
  end

end

class MyBuilder < AbstractBuilder

  def build
    puts "starting build"
    sleep(5)
    puts "built."
  end

  def unnaffected_method
    # this method won't get redefined
  end

end

b = MyBuilder.new
b.build

输出

defining other method unnaffected_method
before
starting build
built.
after

【讨论】:

    【解决方案2】:

    我会和alias_method一起玩:

    module Timeable
      def time_methods *meths
        meths.each do |meth|
          alias_method "old_#{meth}", meth
    
          define_method meth do |*args|
            started_at = Time.now
            res = send "old_#{meth}", *args
            puts "Execution took %f seconds" % (Time.now - started_at)
            res
          end
        end
      end
    
    end
    
    class Foo
      def bar str
        puts str
      end
    end
    
    Foo.extend Timeable
    Foo.time_methods :bar
    Foo.new.bar('asd')
    #=>asd
    #=>Execution took 0.000050 seconds
    

    【讨论】:

    • 这看起来可以工作,我不想调用 time_methods 因为在我的情况下,这是子类中唯一的一个方法,总是同名,我想包装代码。我将尝试在基类的初始化中使用 alias_method 和 define_method。那是假设调用initialize时子类中的方法存在?!?
    • 你把我弄丢了。如果您能描述您要解决的真正问题,这将有所帮助。首先,您是否可以控制父类?你能不能改变build_with_timer的方法?
    • 是的,我控制这两个类,在子类中会有一个名为“build”的方法的约定,当调用该方法时,基类中指定的代码在方法之前和之后运行,之前/之后的代码将始终相同。希望这是有道理的。
    【解决方案3】:

    听起来您正在寻找对象生命周期事件的挂钩。您必须将其构建到您的基础对象中并提供一些 DSL —— 我想您正在寻找类似 @​​987654321@ 的东西。以下是我们如何修改您的示例以允许这样的事情:

    class AbstractBuilder
      attr_reader :time_taken
    
      def construct! # i.e., build, and also call your hooks
        @@prebuild.each { |sym| self.send(sym) }
        build
        @@postbuild.each { |sym| self.send(sym) }
      end  
    
      def construct_with_timer
        started_at = Time.now
        construct!
        @time_taken = Time.now - started_at
    
        puts "!!! Build time: #@time_taken"
      end
    
      class << self
        def before_build(fn); @@prebuild ||= []; @@prebuild << fn; end
        def after_build(fn);  @@postbuild ||= []; @@postbuild << fn; end
      end
    end
    
    class MyBuilder < AbstractBuilder
      before_build :preprocess
      after_build  :postprocess
    
      def build; puts "BUILDING"; sleep(3); end
      def preprocess;  puts "Preparing to build..."; end
      def postprocess; puts "Done building. Thank you for waiting."; end
    end
    
    builder = MyBuilder.new
    builder.construct_with_timer
    
    # => Preparing to build...
    # => BUILDING
    # => Done building. Thank you for waiting.
    # => !!! Build time: 3.000119
    

    【讨论】:

    • 是和否,我想要过滤器之前/之后(或周围)的概念,但只有一种方法,总是具有相同的名称,我需要包装,它将始终包装在相同的代码中。所以我需要一个硬编码版本。从您的示例看来,我需要在类级别存储之前/之后的代码,然后在实例化对象后注入它?
    • 对——前/后逻辑是一种用于快速构建子类的小语言。对于更复杂的需求,请注意 Ruby 本身在这里也提供了一些有用的内部回调。您可以编写名为“inherited”和“included”的方法,只要类被继承或包含,这些方法就会被触发。
    【解决方案4】:

    这是Aspect-Oriented Programming 的教科书定义用例。它通常提供更清晰的关注点分离。在这个领域,Ruby 提供AquariumAspectR。但是,您可能不想向您的项目添加另一个依赖项。因此,您可能仍会考虑使用其他方法之一。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-02
      相关资源
      最近更新 更多