Alias a Static Method in Ruby
Listen to this article
As much as I love Ruby on Rails, one of the things that dissapoints me is the rather large number of static methods. Maybe it's my Java background but class methods just irk me: They're difficult to override, mock, stub-out, and they're inherintly thread-unfriendly. But that aside, the fact remains that they exist and, now and again, I need to mess with them. Case in point: My Foreign Key Migrations plugin.
The plugin was working fantastically—thanks to the efforts of Ted Davis for giving it a work out—until it came time to running functional tests. Unfortunately, but unsurprisingly, the schema dumper used to copy the database structure from development to test creates tables in alphabetical order. Now I can't actually think of a much better alternative but the problem is that this means the foreign key clauses were being generated against tables that possibly didn't exist at the time of execution. (For example, Order sorts before Product however the orders table has a foreign key to the products table.)
So anyway, the longer-term solution is to batch up the foreign-key declarations until the end of the script and then execute them but in the meantime, I just wanted to disable their generation altogether. In either case, I needed to interfere with the execution of ActiveRecord::Schema.define a static (boo, hiss) method. Now, for the fun bit.
In Ruby, the way to safely mix-in methods (rather than use subclassing) is to use some method chaining. For this we use alias_method. So, for example, if we wanted to override to_s to always place quotes around the value (nopt very useful but it will suffice for now) we could write a module like this:
module QuoteToS
def self.included(base)
base.class_eval do
alias_method :to_s_without_quotes, :to_s unless method_defined?(:to_s_without_quotes)
alias_method :to_s, :to_s_with_quotes
end
end
def to_s_with_quotes
"'#{to_s_without_quotes}'"
end
end
This essentially says that when the module is included (ie mixed-in) to a class then: add a method named to_s_with_quotes; create an alias of the existing to_s named to_s_without_quotes; make an alias of the new to_s_with_quotes named to_s; and finally, whenever to_s. is executed, call the old to_s method (now named to_s_without_quotes) and surround the results with, you guess it, quotes.
To use this you would either manually include the module in a class or, more along the lines of aspects, force the inclusion with some code like this:
MyClass.send(:include, QuoteToS)
(As a side note, the use of unless method_defined?(:to_s_without_quotes) is to work-around a bug in Ruby 1.8.4 that causes an infinite recursion when using alias_method. I never detected it under Mac OS X but apparently it affects windows machines with monotonous regularity. D'oh!)
So that's all very well and good but what happens when you need to do the same thing with static methods? The answer is, use class << self and extend. In my case, overriding the behaviour of ActiveRecord::Schema.define looks something like this:
module ForeignKeyMigrations::Schema
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
class << self
alias_method :define_without_fk, :define unless method_defined?(:define_without_fk)
alias_method :define, :define_with_fk
end
end
end
module ClassMethods
def define_with_fk(info={}, &block)
...
define_without_fk(info, &block)
end
end
end
Here, the use of base.extend causes all the methods defined within the module ClassMethods—an aribitrary name used by convention in most if not all the rails code I've ever seen—to be added as static methods on the class. Then, surrounding the alias_method calls within a class << self causes them to be executed in a static context.
Again, to have this code mixed-in to the existing ActiveRecord::Schema class looks like this:
ActiveRecord::Schema.send(:include, ForeignKeyMigrations::Schema)
Phew!
P.S. all the plugins are now available via SVN.
Update: See the comments on how to simplify the code thanks to Ryan Tomayko.
Comments
In your QuoteToS example, shouldn't the body of to_s_with_quotes should actually be:
return "'#{to_s_with_quotes}'"
Posted by: Johan Lind | May 28, 2006 05:49 AM
Doh! I *had* to get that wrong. Of course i meant:
return "'#{to_s_without_quotes}'"
:)
Posted by: Johan Lind | May 28, 2006 05:52 AM
Hey, thanks for the credit :-)
I look forward to the batching feature so that I can FINALLY get referential integrity in my testing database... that's annoyed me about Rails since day one! I have several unit tests written specifically to point-out that referential integrity is broken (they insert orphaned and duplicate records). I long for the day when they all pass :-)
Posted by: Ted | May 28, 2006 06:28 AM
Johan,
Fixed. I was coding examples on-the-fly again and then raced out to watch a movie...d'oh!
Posted by: Simon Harris
|
May 28, 2006 09:09 AM
Simon, while I get what you set out to show in this example, I have to ask you to explain to me what you meant by "Maybe it's my Java background but class methods just irk me: They're difficult to override, mock, stub-out, and they're inherintly thread-unfriendly". Well, :) not so much what you meant - as it is clear as crystal - but could you give me a concrete example of where a class method proved to be difficult to handle as you so mention.
I tried out this following example -
class B
def self.foo
puts "Base"
end
end
class D < B
def self.foo
super
puts "Derieved"
end
end
now - calling D.foo puts out the following
Base
Derieved
which is what one would expect because classes are just objects - just lke anything else. Isn't static the wrong word to use in this context? From the java world atleast static methods are invoked using invokestatic instead of invokevirtual opcodes and the call is determined at compile time (hence the "static" moniker). In a language like Ruby, in which classes behave just like normal everyday objects, what is the real problem with class methods? I agree with the thread safety part - but understanding that the object identified by the constant representing the class is a global/shared resource and should be used as such - should take care of the problem.
I don't mean to come down on you bad, but your posts usually make complete sense, but this one line did not. Could you please clarify what you really meant?
Posted by: Badri | June 1, 2006 03:14 AM
Static isn't the correct term I agree. The correct term is class method but the only analogue in Java is a static method so I guess that's really why I used the term--so those not familiar with Ruby, yet have an understanding of Java, might follow. Maybe that was a mistake on my part. It wasn't the static nature of the method I was really interested in so much as the fact that it's defined at the class level and thus shared across all instances of the class.
As to why I dislike class methods, because they're shared across all instances which makes them inherintly thread un-friendly unless you take extra care; difficult to override not in effort but in effect--if I override it for one I override it for all; and, really an extension of the previous point, difficult to test because if I mock/stub it for one instance then I affect all instances.
In the example I used, I needed to override behaviour of Schema.define--which is a class method--to set some state that is unfortunately shared across all instances and because that state affects the behaviour, then I will inadvertently affect the behaviour of all instances in all threads. As I mentioned, probably not a real cause for concern in this particular case but that's the problem with this class (pardon the overloaded use of the term) of problem: you never know just when it might come back to bite you in the bum :)
I can honestly say I've managed to remove all traces of class methods from all my applications thus far and I believe that the code is cleaner, make's more sense, is easier to debug and maintain. I am certainly of the opinion that, except in a VERY small number of cases, there is no reason to use class methods (or properties for that matter) to share global state; if you wish to share state, create an object and pass it around. This is not to say that they aren't useful, just that they are abused...IMHO :)
Hope that clarifies things a little :)
Simon
Posted by: Simon Harris
|
June 1, 2006 05:35 AM
This might be a bit simpler:
http://www.bigbold.com/snippets/posts/show/2133
Posted by: Ryan Tomayko | June 2, 2006 02:55 AM
Hmm..interesting...anything to remove syntax is a good thing! Thanks for the heads-up. I'll check it out.
Posted by: Simon Harris
|
June 2, 2006 03:02 AM
You can easily mock/stub methods on one instance of the class object quite easily actually, by using the eigenclass of it. Just do MyClass.dup into a variable )which will give you just as good of a class as any, just not assigned to a constant) and then call the class <<var to hop into it's class definition at will.
Posted by: Julik | June 14, 2006 09:40 AM
And I am not Simon Harris, not at all.
Posted by: Julik | June 14, 2006 09:41 AM
Nice tip!
Posted by: Simon Harris
|
June 14, 2006 09:43 AM
Sorry, the way your comments are laid out brought me into confusion as to where the commenter's name is. Too late and too hot here.
Posted by: Julik | June 14, 2006 01:10 PM
Hi Simon,
Can you clarify these comments:
" can honestly say I've managed to remove all traces of class methods from all my applications thus far and I believe that the code is cleaner, make's more sense, is easier to debug and maintain"
Most of the rail's model code (model in rails sense, active record objects) have extended the model behavior via class methods. so, I'd be curious to know how you are avoiding this?
I'm new to Ruby/Rails, and all I can imagine is code that looks like this (just a simple example):
class Visit ["visit_dt >= ? and visit_dt ["visit_dt >= ? and visit_dt < ?", now.at_beginning_of_week, now.next_week ])
end
end
class DashboardController < ApplicationController
def index
@member_visits_this_week = Visit.count_visits_this_week
end
end
Posted by: Dan Hatfield | June 16, 2006 01:07 AM
Hmm, that code didn't come cleanly through..
Basically, I was curious as to how you are extending the behavior of your rails model (objects that extended ActiveRecord) and then how you use those objects in your controllers..
I'm assuming without a class method that you first have to instantiate a model class and then call the instance method?
@model = Model.new
@model.do_something
vs
Model.do_something
Is this correct?
Posted by: Dan Hatfield | June 16, 2006 01:12 AM
Dan,
I was referring mainly to work done in other languages such as Java and C++ (and Ruby in general for that matter) not really Rails--I almost consider Rails another language.
Posted by: Simon Harris
|
June 16, 2006 05:51 AM
I sympathise with your comments about class methods being "difficult to test because if I mock/stub it for one instance then I affect all instances". Have a look at Mocha. It gives you a simple readable way to mock or stub class methods as well as putting the class back together again after each test method.
Posted by: James Mead | August 26, 2006 08:01 AM
Thanks a ton. I was beating my head against the wall trying to redefine a class instance method from a module in a Rails plugin, to no avail. I kept trying different combos of extend and include with the right module nesting, but a little base.class_eval combined with alias_method, from your first example, made it much cleaner and easier.
Posted by: Tim Connor | October 24, 2006 07:16 PM