3

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!

skukx
  • 617
  • 5
  • 15

2 Answers2

2

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.

Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • Thank you, I've never heard of the `refine` method before so I'm definitely going to read up on that – skukx Feb 06 '20 at 18:51
1

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.

skukx
  • 617
  • 5
  • 15