10

I currently have a superclass which has a function that I want all the subclass to call within each of its function. The function is supposed to behave like a before_filter function in rails but I am not sure on how to go about implementing before_filter. Here is an example

class Superclass
  def before_each_method
    puts "Before Method" #this is supposed to be invoked by each extending class' method
  end
end

class Subclass < Superclass
  def my_method
    #when this method is called, before_each_method method is supposed to get invoked
  end
end
denniss
  • 17,229
  • 26
  • 92
  • 141

3 Answers3

13

This is one way to do it:

class Superclass
  def before_each_method name
    p [:before_method, name]
  end

  def self.method_added name
    return if @__last_methods_added && @__last_methods_added.include?(name)
    with = :"#{name}_with_before_each_method"
    without = :"#{name}_without_before_each_method"
    @__last_methods_added = [name, with, without]
    define_method with do |*args, &block|
      before_each_method name
      send without, *args, &block
    end
    alias_method without, name
    alias_method name, with
    @__last_methods_added = nil
  end
end

class SubclassA < Superclass
  def my_new_method
    p :my_new_method
  end

  def my_new_other_method
    p :my_new_other_method
  end
end

SubclassA.new.my_new_method
SubclassA.new.my_new_other_method

This will create a wrapper method using the alias_method_chaining method as soon as the method you'd like to wrap is defined in the subclass.

Ian Vaughan
  • 20,211
  • 13
  • 59
  • 79
Florian Hanke
  • 331
  • 2
  • 5
  • I believe this breaks `super`. That is, if `SubclassB < SubclassA < Superclass`, and if `SubclassA` implements the method `foo`, which `SubclassB` references in a new definition by calling `super`, then every call to the `super` method will result in a `send :foo_without_before_each_method`, and thus `send :foo`, which will look in `SubclassB` (why wouldn't it?), and call another `super`, causing an infinite loop. – preferred_anon Feb 03 '20 at 16:33
  • In contexts other than the original questioner's where the subclass depth is > 1, this will cause an infinite loop, indeed. – Florian Hanke Feb 05 '20 at 14:23
7

This is my solution:

require 'active_support/all'

module BeforeEach
  extend ActiveSupport::Concern

  module InstanceMethods
    def before_each
      raise NotImplementedError('Please define before_each method')
    end
  end

  module ClassMethods
    def method_added(method)
      method = method.to_s.gsub(/_with(out)?_before$/, '')
      with_method, without_method = "#{method}_with_before", "#{method}_without_before"

      return if method == 'before_each' or method_defined?(with_method)

      define_method(with_method) do |*args, &block|
        before_each
        send(without_method, *args, &block)
      end
      alias_method_chain(method, :before)
    end
  end
end

To use it, just include BeforeEach into your class like so:

class Superclass
  include BeforeEach

  def before_each
    puts "Before Method" #this is supposed to be invoked by each extending class' method
  end
end

class Subclass < Superclass
  def my_method
    #when this method is called, before_each_method method is supposed to get invoked
  end
end

Subclass.new.my_method

# => Before Method

Hopefully this will work for you!

Mario Uher
  • 12,249
  • 4
  • 42
  • 68
  • hi, i dont use rails, but this is that i need, i hava problems with the alias_method_chain, how can i replace it? thanks !! – Yamit May 29 '19 at 02:27
1
class BalanceChart < BalanceFind
  include ExecutionHooks

  attr_reader :options

  def initialize(options = {})
    @options = options
    @begin_at = @options[:begin_at]
  end

  def months_used
   range.map{|date| I18n.l date, format: :month_year}.uniq!
  end

  before_hook :months_data, :months_used, :debits_amount
end

module ExecutionHooks

  def self.included(base)
   base.send :extend, ClassMethods
  end

  module ClassMethods

    def before
      @hooks.each do |name|
        m = instance_method(name)
        define_method(name) do |*args, &block|  

          return if @begin_at.blank? ## the code you can execute before methods

          m.bind(self).(*args, &block) ## your old code in the method of the class
        end
      end
    end

    def before_hook(*method_name)
      @hooks = method_name
      before
    end

    def hooks
      @hooks ||= []
    end
  end
end
Andrew Schwartz
  • 4,440
  • 3
  • 25
  • 58
Breno Perucchi
  • 873
  • 7
  • 16