Let’s take a look at the implementation of an Annotating Keyword…
CalculatorTest extends the TestDirective Module, so methods defined in TestDirective will be included as class methods in CalculatorTest. When the interpreter encounters the acceptance_only keyword, it’s recognized as a call to the acceptance_only class method. This method simply sets a class level instance variable indicating wether or not the current build is an acceptance build. The real work is done by method_added, a hook method provided by the Module class. When a class (or module) is defined in Ruby, the actual class or module is created as soon as the interpreter encounters the opening declaration (e.g. “class Foo”). As the interpreter encounters method definitions, it adds methods to the method table of the class. After a new method is added, the method_added class method is called with the method name passed as a parameter. In the Annotating Keyword implementation, method_added checks if the new method is valid based on state set by calls to acceptance_only. If the method is not valid in the current context, method_added removes the method from the class definition. As a final step, state is reset back to “normal”. Note that we could choose to not reset state, making method_added work very much like method accessibility keywords (public/protected/private).
The implementation of an Annotated Block…
The greatest strength of an Annotated Block is its exceedingly straight forward implementation. The method declaration itself is a parameter to the acceptance_only class method. acceptance_only then chooses wether of not to evaluate the method definition based on on a check of the environment variable.
Lastly, here is the implementation of Annotated Method Declaration…
Conceptually, Annotated Method Declaration is very similar to Annotated Block. The difference is that instead of passing a method declaration to the annotating class method, the method body and method name are passed, and the annotating method takes care of the plumbing of actually defining the method.
The CalculatorTest example demonstrates an interpreter directive style use of annotations. These same techniques are useful for more AOPish situations. Take the canonical AOP example of logging. Logging is a cross cutting concern. That is to say, logging code can be found throughout an application, tangled up with the actual work of a method. Take an example of a requirement that certain method calls must logged…
An Annotating Keyword implementation would allow for a better separation of concerns …
The Loggable implementation demonstrates the primary weaknesses of Annotating Keyword, the complexity of it’s implementation…
Briefly, method_added aliases the method to be logged and redefines the accessor to return a method which logs relevant information before calling the original method.
So which style is appropriate where? Annotating Keyword is certainly the most aesthetically pleasant to use, but it suffer from what Anders Hejlsberg calls simplexity. The simple usage syntax hides a complex implementation. Truly simple solutions are simple from top to bottom. This complexity increases when multiple annotations come into play. I’m inclined to use it for interpreter directive style annotations, or where I need to chain AOP style annotations.
Annotated Block is the middle-ground option. It avoids the use of the method_added hook, leaving it free for other uses (some of which may not be fully within the developers control). Like Annotating Keyword, it’s chain-able, albeit only through some unfortunately indented code. Simplicity is a double edged sword here. Because an Annotating Block know so little about the methods it wraps, it is limited in how it can interact with those methods. In particular, it does not have access to the arguments of the methods it wraps. The only way it can supplement its weak position is to maintain some sort of state that interacts with method_added, adding the complexity of Annotating Keyword. I prefer it for simple interpreter directive usages limited to including or excluding the definition of methods or execution of a code block.
Annotated Method Declaration is the only option that allows for code execution before the annotated method is defined. This is a powerful property when coupled with its access to the parameters passed to the called method. The major drawback of Annotated Method Declaration is that it cannot be chained. I prefer to use it for AOP style annotations when an inability to add multiple annotations is acceptable.
Martin Fowler has an entry on RubyAnnotations on his bliki. Be sure to read Jay Field’s recent write up on class method declaration syntax. Thanks to Paul Gross for pointing out that a post on annotations should be part of this series.
Thanks for reading. As always your comments are appreciated.
Friday, October 26th, 2007 at 12:46 pm.
Filed under patterns/idiom/style.
Subscribe to RSS 2.0.
Comments and trackbacks disabled.
October 29th, 2007 at 1:48 pm
Oh, heck, maybe I should have used annotations for my personal Ruby test suite. Right now I’m just doing:
if WINDOWS
def test_something
…
end
end
Annotations would look better for the simple case. But, then I have times where I do stuff like:
if WINDOWS && !JRUBY
def test_someting_jruby_doesn’t_support_on_windows
…
end
end
And that’s just one of several combinations. I don’t know that there’s a generic way to handle that. At least, not one that’s less ugly.
Thanks again for a nice article, btw.
Dan
October 29th, 2007 at 7:54 pm
Hi Daniel,
Thanks again for your comment. If I were eliminating things left and right I might do something like below, but my tolerance for this sort of thing is pretty high as long as the methods are short and the source is in a reasonable place…
def self.exclude(platform_expression)
@excluded_test = platform_match? platform_expression
end
def self.platform_match?(platform_expression)
!platform_expression.find { |key, value| const_from_sym(key) && const_from_sym(value) }.nil?
end
def self.const_from_sym(sym)
Kernel.const_get(sym.to_s.upcase)
end
exclude :windows => :jruby
def test_something_jruby_doesnt_support_on_windows
end
October 30th, 2007 at 9:16 am
Patrick, great blog entry, thanks. Just wondering after reading how the “Annotated Method Declaration” has access to the parameters passed? You mentiion this as a distinguishing point, but don’t actually show it.
I cannot see how “acceptance_only :test_some_complex_calculation do” has any parameters to the new method test_some_complex_calculations, and I am not sure how it could. The only thing I came up with was something heading toward simplexity and that is to actually define two methods. The first would accept *args and then do whatever with them and then call the “acceptance_only” method. Is there another way?
Thanks
November 7th, 2007 at 11:00 am
Well I found an answer to my own comment.
>> module E
>> def accept_only(sym, &body)
>> define_method(sym) { |*args| puts args.inspect; body.call(*args) }
>> end
>> end
=> nil
>> class TestSomething
>> extend E
>> accept_only :setup_something do |x|
?> puts x + 2
>> end
>> end
=> #
>> t = TestSomething.new
=> #
>> TestSomething.instance_methods(false).inspect
=> “[\”setup_something\”]”
>> t.setup_something(1)
[1]
3
=> nil
November 12th, 2007 at 2:57 pm
Hi Johnny,
Took a couple weeks off. Thanks for the nice example.