Lately I’ve been thinking a lot about How and When. Two old friends. They’re pretty quiet, but play a huge role (I think) in making us better at doing what we do.
Consider this: you know how to use alias_method_chain (you read Marcel’s post back in April (if you didn’t just keep reading)), but do you know when? Maybe. Maybe… not. Care to join me for a short journey through the land of Refactoring and Bling? Alias method bling?
Training Wheels (or The How in How&When)
(Skip this section if you already know the How. (!))
The basics are this: you have a method. You want to add functionality to that method by wrapping it. Of course, you’re gonna use alias:
alias :real_name :name def name "My name is #{real_name}." end
As you know, this points real_name to name as it exists at the time of the alias call, allowing you to safely override name while maintaining a reference to the original version.
There are problems with this approach on a big project. Imagine you want to override render to provide benchmarking powers:
alias :real_render :render def render(*args) benchmark("Render call") do real_render(*args) end end
Okay, yeah. Now let’s say you want to layer another bit of functionality onto render. Disaster. alias_method_chain prevents these conflicts of interest:
def render_with_benchmark(*args) benchmark("Render call") do render_without_benchmark(*args) end end alias_method_chain :render, :benchmark
Our little friend will, behind the scenes, alias render to render_without_benchmark then promptly alias render_with_benchmark to render. Now we can add thinks like alias_method_chain :render, :layout and not worry about breaking other aliases. They all layer (safely) on top of one another.
Check the Rails documentation for further examination.
That’s the How. And even the When, in Rails. But When should you apply this?
Well, plugins.
Ballin’ Out of Control (or Refactoring acts_as_cached)
A while ago I released a Rails plugin called acts_as_cached. “This is messy, but it has to be messy!” I told myself. “This is complicated code.” Also: “This is as clean as I can get it.”
Sound familiar? Of course. I knew all the mantras: “Make each method do one thing.” “Separation of concerns.” “Be modular.” Those, however, are just words. Fun to say, but just words. I think in code.
Here’s the old get_from_cache method:
def real_get_from_cache(key) cache_benchmark "Tried to get #{key} from cache." do # Try to grab from cache, lazy load any classes which Rails can't find. # Re-raise non-load related exceptions. begin if cache_config[:local_cache] cache_config[:local_cache][key] ||= cache_store.get(key) else cache_store.get(key) end rescue MemCache::MemCacheError => e # Memcache API swallows const errors. lazy_load ||= Hash.new { |hash, hash_key| hash[hash_key] = true; false } klass_file = e.to_s.split.last.underscore if e.to_s =~ /undefined class/ && !lazy_load[klass_file] require_dependency klass_file retry else raise e end end end if cache_store end
Wow. What. The. Hell.
Take a deep breath. Okay, there are a few things going on here:
- Use the local_cache if it exists
- Lazy load classes (super messy)
- Benchmark the call
- Do nothing if no cache_store exists
- Get from the cache_store
That’s, like, more than one thing. Bad bad bad. How can we clean this up? alias_method_chain, mostly.
Slim Pickin’ (or The Retrofacted get_from_cache)
Without going into any detail yet, here’s the new version. I renamed it fetch_cache and cleaned it up a bit:
def fetch_cache(id) return if ActsAsCached.config[:skip_gets] autoload_missing_constants do cache_config[:store].get(cache_key(id)) end end
What. The. Hell. It still does more than one thing, you could argue, but it’s definitely a lot cleaner and easier to maintain. Where’d all that other code go, though? Did I throw it out? Functionality be damned!
No: I bling’d it.
The Bling (or The Chain)
What I did was identify all the different aspects of get_from_cache and move each bit of functionality into its own module. One for the local_cache stuff, one for disabling the cache, one for benchmarking, etc.
For instance, in the LocalCache module I have fetch_cache_with_local_cache:
def fetch_cache_with_local_cache(*args) @@local_cache[cache_key(args.first)] ||= fetch_cache_without_local_cache(*args) end
Activated like so:
alias_method_chain :fetch_cache, :local_cache
It lovingly wraps the concise, simple fetch_cache I have already shared with you to provide additional functionality.
Likewise, in my Benchmarking module I have fetch_cache_with_benchmarking:
def fetch_cache_with_benchmarking(*args) cache_benchmark "Got #{cache_key args.first} from cache." do fetch_cache_without_benchmarking(*args) end end
Activated like so:
alias_method_chain :fetch_cache, :benchmarking
Etc. If you want to use the local_cache stuff, the alias_method_chain will be called. If you want to use the benchmarking stuff, the alias_method_chain will be called. If you don’t use benchmarking, fetch_cache will be left alone.
You can start to see the benefit of layering different methods on top of each other without interfering with the base functionality. The code is just easier to understand this way.
Three Dollar Bill (or Added Advantages)
While it may seem we’re spreading fetch_cache logic all over the source tree, what we’re really doing is grouping related methods together in convenient, simple modules. There isn’t just fetch_cache_with_benchmarking in the benchmarking module; there’s also set_cache_with_benchmarking and expire_cache_with_benchmarking. All the benchmarking methods are together. All the local_cache methods are together. Etc.
This makes it easy to edit all the benchmarking code at one time, and even easier to add entirely new functionality to select methods. We could drop in a new module, wrap what we need to with alias_method_chain, add a conditional check to the acts_as_cached method to include it, and rock out.
Bonus Round (or Bonus Round)
You may have noticed that my refactored fetch_cache method includes a method called autoload_missing_constants, which takes a block. Rather than keep the re-loading code in my method, I wanted it to have its own method. Right? To keep things clear. Here it is:
def autoload_missing_constants yield rescue ArgumentError, MemCache::MemCacheError => error lazy_load ||= Hash.new { |hash, key| hash[key] = true; false } retry if error.to_s.include?('undefined class') && !lazy_load[error.to_s.split.last.constantize] raise error end
Blocks are fun. This keeps the fetch_cache method even more focused.
Diamond Grills (or Other Resources)
I mentioned the documentation for further alias_method_chain understanding. There’s also some explanation and a few examples in Ezra’s great PDF on the Rails request lifecycle. The cache_fu source is available for perusal, as are various hacks strewn throughout pastie. Finally: technoweenie’s got some bling scattered throughout his plugins. Naturally.
good timing.. I just found the joy of alias_method_chain too.
putting all the alias_method_chain calls into your plugin module’s self.included method, can end up reading like a summary of what features you’ve addded/extended
I have to admit this’ll probably take a couple readings to fully digest, but in the meantime … what’s your recommended strategy for testing these chains? With the individual components in separate modules I’m not sure how I’d keep the testing straightforward.
Thanks.
Good stuff Chris – thanks.
Brittain: Check out the cache_fu tests. They’re BDD, but they’re pretty full. (testing behavior vs methods)
Awesome post! I’m with Brittain in that it’ll take me a few (many) readings (and much playtime) to actually understand. Question! When you say…
The position of render confuses me. Should I take the placement literally? E.g.:
@alias :render :render_without_benchmark alias :render_with_benchmark :render@
Placement aside, what I’m gathering is that alias_method_chain is creating two methods (with and without) that both point to the original method.
Once you’ve AMC’d a method, would you ever called the true original method? Now that you have both render_without_benchmark and render_with_benchmark, is render obsolete? Or… is that kind of the point?
That you want to wrap render in something, so you create two methods, a copy of the original for the purpose of your extended method, leaving the true original untouched for future generations? Is that accurate? Yeesh. =)
Hmm… I think I get it. Here’s what I’ve come up with…
I suppose I understand it. I’m not sure when I’d use it, but I understand it. Thanks again Chris. =D
Holy cow. So much for formatting. Try again…
class Person
end
Person.new.talk(‘Hello’) Person.new.talk_without_shout(‘Hello’) Person.new.talk_without_loudly(‘Hello’)
it’s called gling :)
i don’t fully understand how autoload_missing_constants works. you’ve eliminated the require statement. Is it the case that simply by virtue of constantizing Ruby will require the file? If so, you should put a comment in there or something!
Nick Kallen: You’re on the right track, just know that constantize is a Rails method, not a Ruby one. See this blog post for more info.
The problem is that memcache-client uses Marshal.load and Marshal.unload so Rails’ built in constant autoloading won’t be hit if you try to Marshal.unload a class which hasn’t yet been loaded. So, we give the Rails auto-loading a nudge.
Chime in.