Thursday, February 24, 2011

Using the singleton class to include a module on-the-fly

I still don't entirely understand the syntax for accessing the singleton class, but I have found a use for it. :) The goal is to be able to make an object include a mixin on the fly.

If you do self.class.send(:include,module_name) instead of using the singleton class then the change affects every instance, not just the one you intended.

So it seems to work pretty well. Ruby is all like "sure buddy, have some rope, take as much as you need". :)

# Where the magic happens...
module RuntimeInclude
def get_singleton
class << self
self
end
end
def runtime_include(the_module)
get_singleton.send(:include,the_module)
end
end
module Payments
module DPS
def foo
"dps"
end
end
module EWAY
def foo
"eway"
end
end
end
class Booking
include RuntimeInclude
def lets_go_eway
runtime_include(Payments::EWAY)
end
def lets_go_dps
runtime_include(Payments::DPS)
end
def foo
"none"
end
end
booking1 = Booking.new
puts "booking1 (original): #{booking1.foo}"
booking1.lets_go_eway
puts "booking1 (eway): #{booking1.foo}"
booking2 = Booking.new
booking2.lets_go_dps
puts "booking2 (dps): #{booking2.foo}"
puts "booking1 (still eway): #{booking1.foo}"
# You can flip it but only once. Scary..?
# I'm guessing once a module is included, re-including it does nothing
booking1.lets_go_dps
puts "booking1 (now dps): #{booking1.foo}"
booking1.lets_go_eway
puts "booking1 (is still dps): #{booking1.foo}"
# output:
# booking1 (original): none
# booking1 (eway): eway
# booking2 (dps): dps
# booking1 (still eway): eway
# booking1 (now dps): dps
# booking1 (is still dps): dps

I should say that this technique was designed to be a quick fix to let us work around some legacy code that really should be refactored and fixed properly. I'm not saying this is a sensible design pattern, but it is cool that ruby can do it.

Here is a good explanation of singleton classes in ruby.

No comments: