Reputation: 617
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
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
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