skukx
skukx

Reputation: 617

Running ruby method around existing specified method

I've got an existing library comprised of many services which all respond to the method execute each method does it's logic

class BaseService
   def execute
     raise NotImplementedError
   end
end

class CreateUserService < BaseService
  def execute
    # Some code that does Other stuff
  end
end

class NotifyService < BaseService
  def execute
    # Some code that does other other stuff
  end
end

I would like to do something to accomplish something like:

class BaseService
  around :execute do |&block|
    puts 'Before'
    block.call
    puts 'After'
  end
end

Which then wraps every execute method including child classes so that logic can be performed before and after.

I've done something like this:

module Hooks

  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def around(*symbols, &block)
      to_prepend = build_module(symbols) do |*args, &mblock|
        result = nil

        block.call do
          result = super(*args, &mblock)
        end

        result
      end

      prepend to_prepend
    end

    private

    def build_module(symbols, &block)
      Module.new do
        symbols.each do |symbol|
          define_method(symbol, &block)
        end
      end
    end
  end
end

class BaseService
  include Hooks

  around :execute do |&block|
    puts 'before'
    block.call
    puts 'after'
  end

  # ..
end

However when the around method only gets executed on the base class. I'm guessing this is due to the nature of prepend. The ancestral order looks like:

[<Module>, BaseService, Hooks, ...]
[NotifyService, <Module>, BaseService, Hooks, ...]
etc

Is there a way I can accomplish this? Thank you!

Upvotes: 3

Views: 538

Answers (2)

skukx
skukx

Reputation: 617

What I've ended up doing is something that I'm unsure whether I'm ok with or not.

class BaseService
  include Hooks

  def self.inherited(subclass)
    subclass.around(:execute) do |&block|
      Rails.logger.tagged(tags) do

        block.call

      end
    end
  end
end

This enabled me to apply to all other classes the around functionality to avoid a massive refactor. Not sure if this is the best solution but wanted to post for others to see.

Upvotes: 1

Eric Duminil
Eric Duminil

Reputation: 54223

You don't modify child classes, and you don't call super from their execute methods.

As far as I can tell, there's no reason why CreateUserService#execute should call the wrapped BaseService#execute.

One way to achieve what you want are refinements:

class BaseService
   def execute
     p "BaseService#execute"
   end
end

class CreateUserService < BaseService
  def execute
    p "CreateUserService#execute"
  end
end

class NotifyService < BaseService
  def execute
    p "NotifyService#execute"
  end
end

module WrappedExecute
  [NotifyService, CreateUserService, BaseService].each do |service_class|
    refine service_class do
      def execute
        puts "Before"
        super
        puts "After"
      end
    end
  end
end

CreateUserService.new.execute
#=> "CreateUserService#execute"

using WrappedExecute

CreateUserService.new.execute

# Before
# "CreateUserService#execute"
# After

Note:

  to_prepend = build_module(symbols) do |*args, &mblock|
    result = nil

    block.call do
      result = super(*args, &mblock)
    end

    result
  end

could be replaced by

  to_prepend = build_module(symbols) do |*args, &mblock|
    block.call do
      super(*args, &mblock)
    end
  end

You'd still need to include Hooks in every Service class, though.

Upvotes: 2

Related Questions