From: Edmund Jorgensen Date: 2006-05-16T18:08:23+09:00 Subject: variable class idea and request for critique Hi all, I'm fairly new to Ruby but really enjoying it. I've been working on a medium large project that is non rails but uses ActiveRecord and assorted other pieces of the rails framework. I'd been finding myself having more and more difficulty writing unit tests as I went because many of my classes were calling class methods of other classes internally--such as the models from rails--and these were hard to factor out. For example: class ImportantModel < ActiveRecord::Base end class Processor def self.do_hard_processing(something) #.... end end class SomeClass def a_method #... something = ImportantModel.find_by_a_lot_of_stuff(one, two) processed = Processor.do_hard_processing(something) if _the_time_is_right?(processed) #...etc end end If those had been objects I could have swapped mocks in for them when I wanted to test SomeClass, but since they're classes--constants by definition--I couldn't! I know about fixtures to help with this problem, but sometimes (as in the case of the Processor class) they aren't really possible, and sometimes you really don't want to involve the working of a model...you just want to test SomeClass's logic with some hard coded mock objects. One solution is pretty simple: class SomeClass attr_accessor :important_model attr_accessor :processor def initialize @important_model = ImportantModel @processor = Processor end def a_method something = important_model.find_by_a_lot_of_stuff(one, two) processed = processor.do_hard_processing(something) if _the_time_is_right?(processed) #...etc end end This is an improvement, because now in a testing environment, for instance, you could set sc = SomeClass.new sc.important_model = HardCodedModel #or even sc.imporant_model = mockObjectOfSomeSort #since there's nothing says it has to be a class But there were two things I didn't like: one, the repetetive nature--you're writing some variation of important_model 3 times, for each class you want to make variable Second of all, I wanted to make it easy to decide on the class/object returned conditionally or dynamically, with a block, which can save you a lot of time in testing code (and could be useful in production, for that matter). The solution I came up with was to add a class method called variable_class to Object, which would provide an indirect way to refer to classes like ImportantModel or Processor (or even allow them to be not classes at all but arbitrary objects), and which had support for a block. require 'rubygems' require 'active_support/inflector' class Object def self.variable_class(klass, accessor_name = nil, &block) klass_str = klass.name klass_sym = klass_str.to_sym klass_translated_str = "#{Inflector.underscore(klass_str.tr_s('::','_'))}_class" instance_var_sym = "@#{klass_translated_str}".to_sym get_method_sym = accessor_name ? accessor_name.to_sym : klass_translated_str.to_sym set_method_sym = "#{get_method_sym.to_s}=".to_sym if block_given? define_method(get_method_sym) do block.call(self) end else define_method(get_method_sym) do instance_variable_get(instance_var_sym) || instance_variable_set(instance_var_sym, klass) end define_method(set_method_sym) do |custom_class| instance_variable_set(instance_var_sym, custom_class) end end end Basically, given two classes (here I'm just going to make them classes and not note the object possibility anymore) class One def self.do puts "one" end end class Two def self.do puts "two" end end All the following will work: class SimplestCase variable_class One def test one_class.do end end sc = SimplestCase.new sc.test #"one" sc.one_class = Two sc.test #"two" The default is to make the class variable settable/gettable with the underscored name of the class plus _class. Any :: operators get turned into underscores as well, so Mod::KlassName would become mod_klass_name_class by default. If you need a shorter/different name, you can pass it as a symbol in the second argument slot. class AlmostAsSimple variable_class One, :my_own_name def test my_own_name.do end end sc = AlmostAsSimple.new sc.test #"one" sc.my_own_name = Two sc.test #"two" If you want to use a block, you can do it like so: class BlockExample @@use_two = false def self.go_two @@use_two = true end variable_class One do @@use_two ? Two : One end def test one_class.do end end be = BlockExample.new be.test #"one" BlockExample.go_two be.test #"two" And if you want to use instance rather than class methods / variables in the block, self is passed along for you. class BlockExample2 def initialize @use_two = false end def go_two @use_two = true end def use_two? @use_two end variable_class One do |be2obj| be2obj.use_two? ? Two : One end def test one_class.do end end be2 = BlockExample2.new be2.test #"one" be2.go_two be2.test #"two" Using this approach, I've been able to actually test the UNITS in my unit tests, by being able to swap in mocks for classes as well as objects and isolating the behavior of the class I'm testing. But I wanted to run it by the people on this fine list to see what suggestions people had for it before I put it in too many spots. Is there a much simpler way to accomplish the same? Or something that could be added now to make it more flexible? Or a way to tighten up the code? I want it to be as Ruby-like as possible. Thanks for any critiques or suggestions. Edmund