Wednesday, 16 May 2007

2. It's best to do one thing really, really well. (Rails refactoring with painless AOP)

I like Google philosophy a lot, especially Thing Number 9 :
  • 9. You can be serious without a suit.
, but that's not the subject of this sermon; today I'd like to tell you about Thing Number Two (TNT) :
  • 2. It's best to do one thing really, really well.
- Amen -, and its IT translation : the "Separation Of Concerns". At the humble coder's level, separated concerns make for a more readable code. Counter example:
1
2
3
4
5
6
def fetch_stuff(*args)
  benchmark = Benchmark.measure{
    Stuff.fetch_all
  }
  puts "time spent: #{benchmark} "
end
It's ugly, but it works. It works, but it's ugly: this code does TWO completely unrelated things:
  1. fetch some stuff, and
  2. benchmark an action, any action
We MUST find a way to separate those 2 concerns: The first and main concern is obvious :
1
2
3
def fetch_stuff
  Stuff.fetch_all # <<--- CONCERN 1 
end
The second concern is easy to code
1
2
3
4
5
6
def generic_benchmarking_method
  benchmark = Benchmark.measure
    action_to_measure   # <<--- (concern 1)       
  }
  puts "time spent : #{benchmark}}" # <<--- CONCERN 2
end   
but it's difficult to connect to the first method, because :
  • users of concern 1 should not have to know about concern 2 => they will just call the first method
  • as the benchmarking action is completely independent from the action it measures, this action should be passed as a parameter in some way
The solution: piggy-back the benchmarking method on the fetching method, transparently. Q: how do you piggy back ? A: with Rails' alias_method_chain , and some "convention over configuration magic"** (** hint: it's all in the methods naming) Take a deep breath and watch the mystery unveil :

Before

1
2
3
4
5
6
def fetch_stuff(*args)
  benchmark = Benchmark.measure{
    Stuff.fetch_all
  }
  puts "time spent: #{benchmark} "
end

After

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# concern 1: just do it.
def fetch_stuff
  Stuff.fetch_all
end

# concern 2: just benchmark it.
def fetch_stuff_with_benchmarking(*args)
  benchmark = Benchmark.measure{
  fetch_stuff_without_benchmarking(*args)
  }
  puts "time spent: #{benchmark} "
end

alias_method_chain :fetch_stuff, :benchmarking
That's it : alias_method_chain wrapped benchmarking around fetch_stuff. After line 14 has been parsed, calls to the original fetch_stuff are redirected to fetch_stuff_with_benchmarking, and fetch_stuff_without_benchmarking points to the original fetch_stuff. It's transparent and automatic. All you have to do is respect the naming convention : with and without. That's it. This method was introduced in Rails a year ago to DRY up the internals. There is no reason your code should not benefit shamelessly from it too. Strive for it. You'll thank me later. API : Articles :

0 comments: