2
votes

Here is a small problem I have. Take note that this is a simplified example. Let's say I have a class with several instance methods and I want to log one of the instance methods using a ActiveSupport::Concern:

class Car
  include LogStartEngine

  def start_engine
    # useful thing
  end

  def check_oil
    # useful thing
  end

  def open_doors
    # useful thing
  end      
end

Here is what I first came up for the concern:

module LogStartEngine
  extend ActiveSupport::Concern

  included do
    alias_method_chain :start_engine, :logging
  end

  def start_engine_with_logging
    Rails.logger.info("Starting engine!")

    start_engine_without_logging

    Rails.logger.info("Engine started!")
  end
end

But this will lead to

  NameError: undefined method `start_engine' for class `Car'
    from /Users/david/.gem/ruby/1.9.3/gems/activesupport-4.0.3/lib/active_support/core_ext/module/aliasing.rb:32:in `alias_method'

This is understandable since when LogStartEngine is included, class Car doesn't have any method called start_engine.

I know I could solve this putting include LogStartEngine after method start_engine but I'd like to keep this statement where it is.

So the constraints are:

  • log only the method start_engine, not all the methods.
  • Car just need to include LogStartEngine concern. I'd like to avoid having to call any custom helper methods added by the concern, something like log_method :start_engine.
  • I want to keep the include LogStartEngine statement where it is. I don't want it to be below method start_engine or at the end of the class.
  • This is using Ruby 1.9. So Module#prepend is not a valid solution :)
3

3 Answers

1
votes

After some trials, here is my solution:

module LogStartEngine
  extend ActiveSupport::Concern

  module ClassMethods
    def method_added(_)
      unless instance_methods.include?(:start_engine_without_logging)
        alias_method_chain :start_engine, :logging
      end
    end
  end

  def start_engine_with_logging
    Rails.logger.info("Starting engine!")

    start_engine_without_logging

    Rails.logger.info("Engine started!")
  end
end

My sub question is: is there any other way to achieve that?

1
votes

It's old question, but I found an answer and I write for someone.

module LogStartEngine
  extend ActiveSupport::Concern

  define_method :start_engine_with_logging do
    Rails.logger.info("Starting engine!")
    start_engine_without_logging
    Rails.logger.info("Engine started!")
  end

  included do
    alias_method_chain :start_engine, :logging
  end
end

define_method is point of this approach, it defines method dynamically on included (before alias_method_chain)

0
votes

An alternative would be to use delegation or use http://www.ruby-doc.org/stdlib-2.0/libdoc/forwardable/rdoc/Forwardable.html - then you could compose your objects and combine them, also I assume you could use method_missing or something similar to provide an "automatic" logger.

class Car
  def start_engine
    # wrooom
  end
end

class CarWithLogging
  attr_reader :car

  def initialize(car)
    @car = car
  end

  def start_engine
    Rails.logger.info "starting engine"
    car.start_engine
    Rails.logger.info "engine started"
  end
end

car = CarWithLogging.new(Car.new)
car.start_engine

Update: As an alternative you could use Ruby's prepend (only available since 2.0), so actually AS::Concern is not required.

class Car
  prepend CarLogging
  def start_engine; end
end

module CarLogging
  def start_engine
    puts "before"
    super
    puts "after"
  end
end