AOP: YARBI

I just watched a talk about AOP given by some guy from PARC (Gregor Kiczales), and I have to say the following:

Before I watched this talk, I thought AOP was just another bad idea.  After watching this talk, I realize that AOP is Yet Another Really Bad Idea.  Either that or this guy is just a few steps away from needing a nurse to cut up his food.

A professor at my alma mater has a decent interest in AOP, enough so that he has taught (on more than one occasion) a “special interest” course in it, demonstrating AOP through AspectJ.  I really enjoy taking classes with the professor, so I decided to spend a bit of time familiarizing myself with AOP so that I could sign up for the course.  After some study, I realized, first, that AOP is not a joke.  It looks, acts, and smells like a joke, but it is not a joke.  Well, it might be some type of cosmic joke, like the flying spaghetti monster thought it would be funny for everyone to write kinda ugly code, but it certainly isn’t a joke to the people who spewed it forth.

More seriously, AOP is one of those interesting-at-a-low-level, misses-the-point-in-a-grandiose-way type of ideas.

To be clear, I definitely see the need for something like AOP.  In the introduction of his talk, Kiczales gives an excellent example of some decently annoying code.  You have an “observable” class (in this case, a visual object) that needs to do some notification when it is modified.  It’s annoying to write “PeopleListening.Update()” at the end of every method, for a few reasons:

  • It isn’t first class - in general, your langauge doesn’t have a name for this at the correct semantic abstraction.  Your choices are:
    • Write out what you need each time as statements - not even close to the “right” abstraction
    • Write out what you need each time as a method call - a bit closer, as you get to name what you are doing, but not quite there as the placement/flow of what you are doing doesn’t have a name
    • Ideally, you’d want the name to also describe its place in decorating the method - after it runs, before it runs, if it fails, if an exception is thrown, etc.
  • The semantic isn’t on the method, but in the method.  You are taking a semantic, like “update observers whenever this method is called”, and turning it into “run method blah, check errors, do some updates, oh, and update observers”.
  • A side-effect of the above is that the semantic isn’t obvious - when you mix these things all together, you can’t easily tell that method FooBar updates the observer in certain conditions.

These are really all the same thing - you don’t have the language to make a solution that fits the following criteria:

  1. Your semantic should be distinguishable for the “regular” method logic.  In effect, the body of the method can be thought of as the “meat” of the methods, where the actual work gets done, and this isn’t a stuffed potato.
  2. Your semantic should be obvious to the observer of the method (I should be able to look at the method or maybe the inherited behavior to quickly tell what is going to happen).
  3. Your semantic should be impervious to the method implementation - changing the name of a local variable, for example, shouldn’t break the method.  Put another way, your semantic should depend only on the obviously available information (at the method level) - class state, static state, arguments, return value, and exceptions.
  4. Your semantic should be well-named, such that its effects are obvious.

In the talk, Kiczales talks about how classes and methods don’t get you what you want (which is true).  He then goes on to make what I think of as the AOP blunder, in that he misses, well, everything but #1.  I’ll put my further thoughts on the talk at the bottom of this post, but I’m going to instead advocate what I feel is the best (two-part) solution.  If you really want to watch me take a crap on AOP, scroll to the bottom.  Otherwise, I’m going to be constructive.

A (Real) Possible Solution

First, we separate the world of “aspects” (I’ll retain the name) into two different types:

First, we have aspects that fundamentally change the overall behavior of a method - the fact that “Add” should notify observers of List is a fundamental part of that method, and removing that behavior would “change” the List object from the perspective of outside observers (or would violate the contract it fulfills).  To take a C# example, the fact that Debug.Assert only gets called in “debug” mode (where DEBUG is defined, I think) fundamentally changes the behavior of the method, especially when you aren’t in debug mode.

Second, we have aspects that are relatively orthogonal to the method/object they decorate.  The logging example that is the (only?) AOP example, is, in my mind, an example of this (the difference between the parts of this dichotomy are really matters of opinion).  Whether or not something is logged is more a function of the logger and not the thing being logged, in a sense.  In the case of observable collections, you could imagine combining these two - logic that logs every time a collection is changed.  The fact that events are sent out is a function of the List object, but the fact that they are then written to disk in a file is a function of the logging logic.

For the first case (change the behavior), we should use annotations (or, for non-Java languages - a metadata facility that lets you “tag” methods with information). You might put an annotation on Collection.add (or some other base class/interface) that says (taken from my Observable Collections package):

@Adder(ElementLocation.FIRST_ARG)
boolean add(E e);

Or, in the more specific case of lists, where you add an element to a specific place, take add and remove:

@Adder(
value = ElementLocation.SECOND_ARG,
indexType = IndexType.INTEGRAL,
index = ElementLocation.FIRST_ARG)
public void add(int index, E element);

@Remover(
value = ElementLocation.RETURN_VAL,
indexType = IndexType.INTEGRAL,
index = ElementLocation.FIRST_ARG)
public E remove(int index);

For the second case (doesn’t change the behavior), something like defadvice is what you want.  “defadvice” (and defmethod with the optional :before/:after, etc.) is a way of some unrelated piece of logic to say, “Before/after/around this specific method call, do the following.”  If the content of this “advice” doesn’t really affect the behavior of the method, then it doesn’t need to be directly annotated on the method.  However, note that defadvice/defmethod uses don’t necessarily follow this, ahem, advice, as they are often used for “altering” behavior (and fail, in these cases, in the same way that AOP fails).  In essence, defadvice is like listening for a pre/post event, where the pre-event may be cancellable.

Each of these areas have downsides as well, which is why you should have two pieces of functionality.

Annotations can “get in the way”, and need to be placed on the class by the class writer.  Sometimes the first is undesired (”the reason I’m using aspects is because I don’t want to write ‘LogThisPlzKThx’ in front of every method”), and you can’t always ensure you are the latter.  If you are just an observer (in the more general sense - you want to act/react to a method, but not muck around with the method itself), then you shouldn’t have to be the guy writing the method itself.

“Defadvice” is nonobvious when looking at the method it is decorating, and it can break if the method is changed either superficially (changed the name, perhaps) or if the class changes (say, introduces a new method that should also be logged).  Tools can help, but I don’t like the idea of relying on tooling for something to be useful (now that is decidedly un-Java-ish).

However, by combining these two, you can provide aspects that meet the above criteria (via annotations) and a nice metaprogramming facility to let you participate in a method call without being an a priori part of that method call.

I’m sure this idea is decades old, and I’m sure much smarter people than me have already played around with it and figured it out (like the smart people at PARC, who must have left before Kiczales came along).  Just think about it, ok?  Next time you see a language problem, picture the code you’d like to write or work with, and figure out how to take your language there.  That’s the real solution.

—-

And now, for the still curious: here are the AOP blunders as told through AspectJ:

In essence, AspectJ is the assembly language by which you can write the above code.  The problem with it being an assembly language, however, is that it is the wrong kind of extensibility - it gives you part of the higher-level abstraction, but in a rather unclean-feeling way.

A good portion of AOP/AspectJ (the portion that Kiczales is pushing us towards, away from the part of AspectJ that isn’t, well, ugly) is very nonobvious.  In a sense, Kiczales is telling us to use AspectJ just like a more powerful / less safe “defadvice” (you can say things like “match all methods that begin with ’set’”, which sends shivers down my spine).  This means that I can’t tell what a method is doing (say, notifying observers) by looking at the method.  I find this to be worse, in the end, than just adding a method call to every method you want to decorate.  Yes, you’ll end up writing it lots of time, and you’ll have some copy/paste errors, but at least the observer of your code will be able to figure out just what the hell is going on.  “defadvice” is only obvious to the person writing the advice.

Really, you can shorten the problem with Kiczales’s presentation down to the simple point that he is pushing “sloppy” defadvice as the one solution (by sloppy, I mean that it doesn’t have to match on a method by method basis, and that you can do nasty things like “match set*”).

First off, I’m not convinced that the simple :before/:after/:around isn’t enough (he proudly notes that there are 11 different places for joinsomethingorothers).  More complicated is generally worse, and I don’t see a reason to provide so many extension points at the same level of abstraction (analogy: you can do really strange and powerful things like this in CLOS, but you have to get into the Metaobject Protocol to do so).

Secondly, I get the feeling that Kiczales is arguing for writing as little code as possible, not in the Lisp way, but in the obfuscated Perl way.  Write these scarily “powerful” (again, powerful like a car crusher, not like a strong laser) statements that do lots of stuff.  Of course, it might be hard to figure out where the stuff is all happening (modulo lots of tooling), or what is causing the stuff to happen (from the perspective of the observer), but it doesn’t matter.  It can crush cars.

Honestly, my biggest problem with AOP (or, more specifically, Kiczales’s AOP) is that it just feels wrong.  It feels like the COME FROM statement, proposed as a joke for FORTRAN.  It feels messy, lacking containment, entirely non-obvious, and just silly.  I hate writing code over-and-over again, which is why I love Lisp.  I don’t, however, enjoy writing Perl in the same way, even though my Perl programs can approximate the size of my Lisp programs in terms of compressed source code size.  That doesn’t mean, however, that my Perl programs compress semantically in the same way; only lexically, and that is what AOP gives us.  They are solving the right problem, but using the wrong solution.

 

Trackbacks

(Trackback URL)

close Reblog this comment
blog comments powered by Disqus

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License.