Advisor: Solve your cross-cutting concerns without mumbo-jumbo.
Advisor
is Ruby gem that enables you to solve cross-cutting concerns without
the usual method-renaming
present in most alternatives.
Advisor
intercepts method calls and allows you to mix cross cutting concerns
and tedious book-keeping tasks. Logging, metric reporting, auditing, timing,
timeouting can be handled beautifully.
Advisor
works with plain Ruby modules and do not mess your stack trace.
Also, the amount of intrusion required to set up is kept to a minimum while still keeping it discoverable. Every affected class must explicitly extend a given module and every affected method call must also be declared.
Usage
Advisor
is organized between two main concepts only: Advisor modules
and
Advice modules
. Advisor modules
are extensions applied to your classes
and Advice modules
define the actual behavior of the intercepted method
calls.
In order to understand better how Advisor
works, we are going to use an
example:
Example
Suppose you want to log calls to some methods but don’t want to keep
repeating the message formatting or messing with the method body.
Advisor
provides a simple built-in module called Advisor::Loggable
that solves this issue.
class Account
extend Advisor::Loggable
log_calls_to :deposit
def deposit(_amount, _origin)
#...
:done
end
end
In an interactive console:
$ Account.new.deposit(300, 'Jane Doe')
# => I, [2015-04-11T21:26:42.405180 #13840] INFO -- : [Time=2015-04-11 21:26:42 -0300][Thread=70183196300040]Called: Account#deposit(300, "Jane Doe")
# => :done
As you can see, the method call is intercepted and a message is printed to
stdout
.
Advisor
achieves this by using Ruby 2.0’s Module#prepend
. If you were
to check Account
’s ancestors you would get:
$ Account.ancestors
# => [Advisor::Advices::CallLogger(deposit), Account, Object, Kernel, BasicObject]
As you can see, the Advisor::Advices::CallLogger(deposit)
module is
listed before Account itself in the ancestor chain.
In the next session we are going to explain how to write your own custom advice.
Writing an Advice
An Advice
defines what to do with the advised method call.
The required interface for an advice must be like the example bellow:
class Advice
def initialize(receiver, advised_method, call_args, **options)
# The constructor of an advice must receive 3 arguments and extra options.
# Those extra options are defined when applying the extension to the advised
# class.
end
def self.applier_method
# Must return the name of the method which must be called in the class body
# to define which methods will be intercepted with the advice.
# In the case of `Advisor::Loggable`, this method returns `:log_calls_to`
end
def call
# This is the body of the advice.
#
# This method will always be called with the block `{ super(*call_args,
# &blk) }` That means the method implementation can decide when to run the
# advised method call. Check `Advisor::Advices::CallLogger` for an example.
end
end
Creating an Advisor
module
Every Advisor
module must be built from the corresponding Advice
by
using the Advisor::Factory#build
method.
Advisor::Loggable
is built from the Advisor::Advices::CallLogger
module.
Advisor::Loggable
itself is built like this:
module Advisor
Loggable = Factory.new(Advices::CallLogger).build
end
Hence, if your custom Advice
complies to the required interface,
Advisor::Factory
will be able to convert it to an extension module with
no problems.
Disclaimer
This version of the library is still experimental and probably not production ready. Use at your own risk.