ruby picture

RCR 321: Aspect Oriented Programming for Ruby

Submitted by transami (Sun Oct 16 20:34:30 UTC 2005)

Abstract

This RCR presents cut-based AOP, an efficient and easy-to-use approach to Aspect Oriented Programming for Ruby.

The work herein is the culmination of multi-year discussion and inquiry on the topic of AOP for Ruby. It has been carried-out with the ultimate hope of establishing Ruby as a premier AOP language, indeed the AOP language of choice. Since AOP is a very powerful paradigm for abstracting programming solutions into separate concerns, and shows great promise for improvements in code maintenance and reusability, it seems only natural that an agile language such as Ruby would provide support for this increasingly popular pattern of design.

Problem

While Ruby's meta-programming facilities are powerful enough to allow for AOP-esque techniques, Ruby's lack of any dedicated AOP support makes it difficult and inefficient to apply Aspect Oriented Programming principles to application development and makes it practically impossible to do so in any conventional and thus generally reusable way.

Overview of AOP

In AOP, one considers aspects of concern applicable across multiple classes and methods. Thus AOP is said to address cross-cutting concerns. Aspects consist of advice, which are methods designed to intercept other methods or events according to specified criteria. This criteria is called a point-cut and it designates a set of join-points. A join-point (or code-point) is the specific place within a program's execution where the advice can be inserted. In this way, AOP is thought to provide a means of organizing code orthogonal to OOP techniques.


    
            ^
            |
       OOP  |   Prob Set.
            |     
            +------------->
                  AOP

    

The overall concept is very powerful, but likewise it can be difficult to integrate into an underlying system, easily succumbing to limitations in efficiency and subverting the intended ease-of-use and reusability. For these reasons we believe AOP has not yet become widespread. Our design addresses these issues.

Qualifications for AOP

To qualify as an AOP capable language, the following criteria must be given considerable support:

The above four points are the functional criteria of any implementation of AOP. In addition there are three major means of implementation:

While the capabilities of these basis largely overlap, they admit of enough distinctions to justify independent support in accordance to the needs of the language. The first of these is generally ill suited to a highly dynamic language like Ruby (although we have recently determined that a hybrid of the first and last may be feasible), and Ruby already has some support for the third basis, albeit limited, via set_trace_func, but Ruby is hampered on the second count. This RCR focuses on the second basis, which is really the most suitable to a dynamic language like Ruby.

Design Principles

A mention before getting into the heart of this proposal: The development of this RCR has been guided by the following two important principles:

Proposal

The Cut

The first and foremost requirement of AOP is interception. A few years ago it occurred to us that subclassing itself is very similar to interception. The difference was merely a matter of the visibility of the subclass. With interception, the subclass needed to have its effect transparently. Indeed, Transparent subclassing is the fundamental proposition of this RCR. To accomplish it in Ruby we propose to introduce a new class called the Cut. A cut is a primitive unit of aspecting. It is used to encapsulate advice for a single class. Cuts are self-contained units much like classes and therefore can have their own state (introduction) as well as private auxiliary methods. Although the Cut class is very similar to the Class class, it cannot be instantiated. Rather it is used solely as an "invisible overrider". An example will help clarify.

Given a class C:


    
  class C
    def m1(*args); 1; end
    def m2(*args); 2; end
  end

    

One would normally subclass C in order to gain new functionality.


    
  class A < C
    def m1
      print '{', super, '}'
    end
  end
  A.new.m1  #=> {1}

    

But unlike a regular subclass, a cut acts transparently. So we introduce the 'cut' construction as follows.


    
  cut A < C
    def m1
      print '{', super, '}'
    end
  end
  C.new.m1  #=> {1}

    

Now, even though we have instantiated class C, we have the functional equivalent of the subclass of C, namely A. Another way of saying this is that we have cut-across the behaviour of C with A. The cut is advantageous in its fine control of how advice interact with the intercepted class and its simple conformity to OOP design. By utilization of the cut AOP begins to flow naturally into ones programs.

Because the Cut is essentially Class, like a Class it can also be defined anonymously, either through instantiation or as a special singleton. The anonymous definition can be especially convenient for internal wraps; useful for assertion checks, temporary tests, etc.

class C
  def m1; 9; end
  Cut.new(self) do
    def m1
      '{' + super + '}'
    end  
  end
end

C.new.m1 #=> {9}

Or through the special singleton form,


    
  c = Object.new
  def c.m1; 8; end
  cut << c
    def m1
      '{' + super + '}'
    end
  end
  c.m1  #=> {8}

    

Additionally, Cuts exist in proxy form to allow modules to be "premixed". This is analogous to proxy classes which allow modules to mixin to the class hierarchy. So too does a proxy-cut include a module, albeit preclusive rather the inclusive in its effect. We offer the module command #preclude to serve as designator of this purpose.


    
  module A
    def m ; "<#{super}>" ; end
  end
  Class T
    preclude A
    def m ; "okay" ; end
  end
  T.new.m  #=> "<okay>"

    

The Cut class is at the heart of this proposal. The remaining sections build on this basic device, demonstrating how to use it for AOP, and offers some important complementary suggestions to make Ruby more convenient with regard to it and AOP requirements in general.

Crosscutting & Targeting

A cut is useful for applying advice which intercept the methods of a single class. But to provide the full advantage of AOP we must also be able to cut-across multiple classes. The simplest means of cross-cutting is by use of a shared module. A shared module can serve as a simple aspect by its inclusion in a cut for each class.


    
  class C
    def x; 'C'; end
  end
  class D
    def x; 'D'; end
  end
  module A
    def x
      '{' + super + '}'
    end
  end
  cut Ac < C ; include A ; end
  cut Ad < D ; include A ; end
  C.new.x  #-> {C}
  D.new.x  #-> {D}

    

Using a cut, advice intercept methods of the same name and use #super to call back to those methods --the basics of subclassing. But for advice to be fully reusable, a way to specify alternate method-to-advice mappings is needed. This can be done by calling secondary methods, as one might normally do within a class.


    
  cut A < C
    def x
      bracket
    end
    def y
      bracket
    end
    def bracket
      '{' + super + '}'  # PROBLEM!
    end
  end

    

But notice the problem that arises. Super will not be directed to m1 or m2 in class C, but to bracket which isn't defined in C. This is not the desired result. A presently possible way to correct this is to pass a closure on the super call of the target method.


    
  cut A < C
    def x
      bracket( lambda{super} )
    end
    def y
      bracket( lambda{super} )
    end
    def bracket( target )
      '{' + target.call + '}'
    end
  end

    

This works well enough, but it is a rather brutish; nor does it provide any significant inspection. An improvement is to pass the target method itself, but enhanced to provide the current super context, and usefully, its own name.


    
  cut A < C
    def m1
      bracket( method(:m1) )
    end
    def m2
      bracket( method(:m2) )
    end
    def bracket( target )
      puts 'Advising #{target.name}...' 
      '{' + target.super + '}'
    end
  end

    

In AOP, the above technique is such a common occurance, that it would be of benefit to further introduce a specail method for accessing the current running method, perhaps the word this. With "this" in place the above example can be nicely simplified.


    
  cut A < C
    def m1
      bracket( this )
    end
    def m2
      bracket( this )
    end
    def bracket( target )
      puts 'Advising #{target.name}... 
      '{' + target.super + '}'
    end
  end

    

In addition, since advising multiple methods with alternate advice, as we have done in the above example, is also such a common case of AOP, a helper method can make it even more convenient. For this we suggest a relatively trivial routine Module#redirect_advice.


    
  cut A < C
    redirect_advice :m1 => :bracket, :m2 => :bracket
    def bracket( target )
      '{' + target.super + '}'
    end
  end

    

Reusability by Dynamic Module Revision

There is an important issue to consider with using redirected advice or, more importantly, when using modules as reusable aspects: care must be taken in choosing method names so as not to inadvertently interfere with the methods of the class(es) being cut. This is problem because it inhibits code reuse, i.e. the ability to design components without regard to where they may be applied. For example:


    
  class C
    def m ; "M" ; end
    def w ; "W" ; end
    def d ; w ; end
  end
  module MA
    def w( target )
      '{' + target.super + '}'
    end
  end
  cut A < C
    include MA
    def m ; w( this ) ; end
  end
  C.new.d  #=> "{W}"

    

In this case, #d does not return "W" as expected, but rather "{W}" because the advice in MA caused a name clash with the #w method in C. To fulfill the true abstraction and reusability potential of AOP this issue must be remedied. And it is a difficult issue that has been a great source of debate and deep consideration in determining how to provide a solution. It was actually David Black who provided the missing concept needed to form a final reasonable solution.

I think if you're inheriting from a class [or module] the burden is on you to know what that class's instance methods are. It's hard to imagine a case where it would be otherwise.

With this idea we have derived a suitable approach from Ruby's ability to dynamically manipulate class/module definitions on the fly, in other words "subclassing" the module and applying any required revisions to avoid unwanted name clash.


    
  class C
    def m ; "M" ; end
    def w ; "W" ; end
    def d ; w ; end
  end
  module MA
    def w( target )
      '{' + target.super + '}'
    end
  end
  module MArC
    include MA
    rename_method :q, :w
  end
  cut A < C
    include MArC
    def m ; q( this ) ; end
  end

    

This solves the problem in a very controllable way, which is nice. Yet it is somewhat long winded. To make it more convenient we can introduce a special form of include that anonymously does the revisions prior to inclusion.


    
  cut A < C
    include_revision MA do
      rename_method :q, :w
    end
    def m ; q( this ) ; end
  end

    

With this solution, the reuse of independently defined aspect modules is readily doable.

System-wide AOP

Typical AOP systems are designed around system-wide effects via specialized join-points and global definitions of pointcuts. Our approach has purposefully avoided this, instead providing a direct "atomic" application of advice to individual classes ans methods, which in turn allows for a more efficient implementation. We believe this has many advantages over the prevalent top-down approaches. Yet we do not exclude the larger functionality either. Thanks to the reflexion Ruby grants by way of ObjectSpace, we can easily cut across large swaths of classes.


    
  ObjectSpace.each_object(Class) { |c|
    if c.instance_methods(false).include?(:to_s)
      Cut.new(c) do
        def :to_s
          super.upcase + "!"
        end
      end
    end
  end
  "a lot of shouting for joy".to_s  #=> "A LOT OF SHOUTING FOR JOY!"

    

There are plenty of great applications for broad cross-cuts like this, especially in the way of code inspection, unit testing, debugging, etc. However ObjectSpace does have some limitations. For instance, Fixnum, Symbol, NilClass TrueClass nor FalseClass actually belong to the ObjectSpace. While minor, we hope these limitation can be removed in the future to foster complete AOP coverage. In addition, some other methods may of use in this context, such as a list of #predecessors, analogous to #ancestors, or a method that mirrors #is_a?, and so on.

Analysis

The Cut and its supporting infrastructure as described above is designed to be a very robust, easy to use, and efficient, providing better overall AOP support than any other language presently in common use.

In contrast, the traditional approach taken by the most AOP systems today, largely propagated by early implementations like Aspect/J, have proven unwieldy and ironically end-up inhibiting code reuse. Infact, the limited reusabiliy has been speculated elsewhere as a potential primary culprit in the limited penetration of AOP to date. This proposal circumvents these issues by offering a general solution directly integrated into the OOP system, rather than attempting to operate wholly beyond it.

Cuts also trump simple method-wrapping mechanisms, like those proposed in Matz' RUbyConf 2004 presentation. While method hooks are especially convenient, they are weak with regards to SOC (Separation Of Concerns); most notably, method hooks lack introduction altogether. They also suffer from order of execution ambiguities that must be dealt with by imposing limitations or adding increasingly specialized declarations. Cuts again circumvent these issues by utilizing an inherent OOP construct --the subclass, rather than adding on a new, wholly "other" entity.

Pros

Cons

Implementation

One implementation detail, not specifically decided by this proposal, is whether cuts may or may not be applied to other cuts. If not allowed, once a cut is applied to a class, a subsequent cut can not be slipped in between it and that class. Cuts are intended to work transparently and offering this feature could thwart this principle. On the other hand, if allowed, it would provided a means for a cut to "underwrite" another cut providing greater flexibility in "meta"-controlling the effects of cuts.

Another implementation detail to consider that falls outside the strict scope of this proposal, but that goes a long way toward bolstering it, is the limits on introduction due to the non-locality of instance variables and methods. Presently cuts will only be able to provide introduction through class varaibles --useful but weak by comparision. With the advent of locals in a future version of Ruby, cuts would gain robust introduction strengths.

The first real step in implementation is, of course, the creation of the transparent subclass, the Cut. This requires an addition in the structure of an object's class hierarchy; essentially a new pointer to a chain of cuts, the last pointing back at the cut class itself --a very simpleton explination to be sure. But fortunately, a well written Ruby patch has been coded by Peter Vanbroekhoven. It implements most of the core funtionality described here, and should serve as a means to investigate and test the potential utility of this RCR. It may also serve as a basis for including these AOP features into Ruby proper, should this RCR be accepted. The patch can be downloaded and found under "cut (transparent subclass)".

ruby picture
Comments Current voting
After I had looked at AspectJ, I had found that aspects, although an interesting idea, seemed to break object-orientation rather than enhance it. This implementation, on the other hand, is strongly based on OOP, and thus works very well with it! By using existing concepts like subclassing, Cuts also make AOP much easier to understand than with AspectJ's implementation, and thus much easier to use. I agree with Trans here that such an implementation could just be what is needed to make AOP more accepted.

Also, a thing I like about this proposal is that it is completely unintrusive (people who don't want to learn about AOP can just ignore Cuts, and everything else will just work as it used to) and doesn't need a preprocessor (unlike AspectJ for instance).

This RCR is well thought-out, researched and argued. A true work of art! I've gladly voted "strongly advocate".


Sorry, that was me speaking here above (Christophe Grandsire).


I am not opposed to AOP in Ruby. Nor am I opposed to the principle of "cuts" for the implementation of AOP in Ruby. I however voted opposed to the RCR in its current form due to a major flaw (in my mind) of the current implementation of cuts.

The flaw is this: a cut made on a base class is always inserted immediately under the base class in the inheritance heirarchy; that is, advice from the cut is always imposed between the aspected classes method and any redefinition/extension of that method in subclasses. In my view of AOP, it should be possible to have the advice atomically surround the method, no matter the depth below the actual cut point.

Take this (contrived) example:


          
  class Foo
    def name
      "Foo"
    end
  end
  class Bar < Foo
    def name
      "Bar < " + super
    end
  end
  cut BracketName < Foo
    def name
      "[" + super + "]"
    end
  end
  Bar.new.name

          

In the current implementation, the advice is applied strictly to Foo#name, including the call to super from Bar#name. Result:

  "Bar < [Foo]"

In some cases this is fine, but in other cases I would expect the advice to be applied around the call to Bar#name with the internal call to super (Foo#name) untouched, yielding:

  "[Bar < Foo]"

In order to vote in support of an implementation of AOP in Ruby, I would require both versions of advice to be supported, with a reasonable way of distinguising the two.

I *do* want AOP in Ruby, and like the concept of "cuts" you have introduced here. Congratulations and many thanks for the work you've done so far. If you expand the RCR to include the functionality described above, I will gladly change my vote in favor of the RCR.

Jacob Fugal lukfugl@gmail.com


Just like matz, I like this part of the proposal:

  module A
    def m ; "<#{super}>" ; end
  end
  Class T
    preclude A
    def m ; "okay" ; end
  end
  T.new.m  #=> "<okay>"

Although I don't like the name. I think "wrap" would be more descriptive. If the implementation allows, it might be useful to be able to manipulate these wrapped modules after the fact: excluding the wrapped modules, redefining/removing methods in the wrapped modules, etc. But, if that becomes too complex or too much overhead, I think it would be fine to not give all that functionality. Starting with a baby step of just doing the above would be a great thing. This RCR to me is too big of a step and will face resistance.

I'm voting opposed because the "cut" concept and syntax doesn't seem very intuitive. I'd rather see the "preclude" above be a basis instead. Just with the above code you can almost immediately grasp the concept and it fits very well into what we already have.

Eric


Hi Jacob,

Intercepting a method in a subclass by defining "around" on a parent class is in a sense "dangerous" b/c a class would then be preventd from being subclassed properly. I'm not saying it can't be useful, but it should be considered carefully. This has come up in our discussions prior, and even with the stated danger set aside, it still befalls the 80/20 rule. It's not the most common use case. But fear not! An easy way achieve this behavior is with a singleton class, when you initialize a class have it define a singleton:

  module M
    def x ; '{' + super + '}' ; end
  end
  class C
    def initialize
      class << self
        include M
      end
    end
    def x ; 'X' ; end
  end
  p C.new.x  #=> {X}

You can continue this by calling super in any subclass:

  class D < C
    def initialize
      super
    end
  end

Considering what you propose and how if effects things, this is a much safer approach to accomplishing it. (And of course you can get even fancier and use something like Facet's preinclusion system ;)

Thanks for bringing this up.

T.


BTW, I do appreciate the thought and time that you guys put into this. Especially making a patch - not many go to that length. The patch also allows us to try out both "cut" and "preclude", right?

Eric


Eric, Just cuts at the moment. Using __cut__ keyword or Cut.new. I suspect Peter can whip up preclude pretty easily using proxy-cut. I'll ask him about it and see if he has time ( he's the c coder in the family ;) BTW there's a switch you can activate on compilation which allows one to cut modules too, just as one cuts classes, but it has a bad speed hit.

T.


Trans,

That does indeed work, and very well. I agree that this is a cleaner and safer approach to the "wrap the entire subtree" use case. Thanks for the feedback!

Changing my vote...

Jacob Fugal


I don't really understand why we need to do 'cut A < C' - if cuts are transparent we shouldn't need to assign them. 'cut C' should work just as well (the way I see it anyway, I might be wrong ofcourse; don't know much about AOP).


The cut name gives you a "handle". You can modify cuts dynamically just as you can classes and modules so giving the cut a name makes that possible. Eg.

  Cut A < C
    ...
  end
  # elsewhere
  Cut A
    ...
  end

Of course you can create annonymous cuts too:

  Cut.new(C) do
    ...
  end


Strongly opposed 1
Opposed 2
Neutral 0
In favor 1
Strongly advocate 10
ruby picture
If you have registered at RCRchive, you may now sign in below. If you have not registered, you may sign up for a username and password. Registering enables you to submit new RCRs, and vote and leave comments on existing RCRs.
Your username:
Your password:

ruby picture

Powered by .