advisor icon indicating copy to clipboard operation
advisor copied to clipboard

AOP without method renaming

  • Advisor: Solve your cross-cutting concerns without mumbo-jumbo.

    [[https://travis-ci.org/rranelli/advisor.svg?branch=master][https://travis-ci.org/rranelli/advisor.svg]]

    =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.

  #+begin_src ruby
  class Account
    extend Advisor::Loggable

    log_calls_to :deposit

    def deposit(_amount, _origin)
      #...
      :done
    end
  end
  #+end_src

  In an interactive console:

  #+begin_src ruby
  $ 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
  #+end_src

  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:

  #+begin_src ruby
  $ Account.ancestors
  # => [Advisor::Advices::CallLogger(deposit), Account, Object, Kernel, BasicObject]
  #+end_src

  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:

  #+begin_src ruby
  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
  #+end_src

***** 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:

  #+begin_src ruby
  module Advisor
    Loggable = Factory.new(Advices::CallLogger).build
  end
  #+end_src

  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.