Il y a des jours où vraiment Ruby me simplifie la vie. Ce qui suit n’est pas révolutionnaire mais je suis tellement content de gagner des heures de codes et de tests grâce à la souplesse de Ruby que je ne peux m’empêcher de vous en faire part.
Voici le problème :
Je possède un plugin contenant plusieurs modèles « métiers » que j’utilise dans plusieurs sites/applications.
Et dernièrement, je souhaite ajouter un attribut commun à tous ces modèles. Jusque là rien de bien compliqué, mais juste pour une seule application, c’est à dire sans avoir d’impact sur les autres applications.
Ex :
foo.rb :
class Foo < ActiveRecord::Base has_many :bars end
bar.rb :
class Bar < ActiveRecord::Base belongs_to :foo end
Ensuite vous avez une classe qui gère des tas de Foo et de Bar :
class MyScript
def initialize(foo_name, bar_name)
@foo_name = foo_name
@bar_name = bar_name
end
def do_a_lot_things
...
# a lot of things before
if foo = Foo.find_by_name(@foo_name)
...
end
if bar = Bar.find_by_name(@bar_name)
...
end
# a lot of things after
...
end
end
ok. maintenant j’ajoute un champ commun : domain une sorte d’enum contenant par exemple : ['Sport', 'Economy', 'Culture']
>> Foo.new.domain = 'Sport' >> a_bar =Bar.find(:first) >> a_bar.domain 'Culture'
bref simple … mais je veux cloisonner mon script en fonction du domaine. il faudrait que je fasse :
class MyScript
...
def do_a_lot_things
...
# a lot of things before
if foo = Foo.find_by_name_and_domain(@foo_name, @domain)
...
end
if bar = Bar.find_by_name_and_domaine(@bar_name, @domain)
...
end
# a lot of things after
...
end
end
Ok j’ai bien fait mes tests, la refactorisation ne devrait pas être compliquée … ah mais non ! mon code doit être compatible pour les applications qui utilise Foo, Bar et MyScript sans utilser le domaine.
On y arrive .. Heureusement ruby et rails mettent à disposition plusieurs outils qui permettent détendre sans tout casser.
Première piste : alias_method_chain
@@domaine = 'Sport'
....
def find_with_domaine(*args)
find_options = {}
if @domaine
find_options[:conditions] = [ 'domaine = ? ', @@domaine ]
end
with_scope(:find => find_options) do
find_without_domaine(*args)
end
end
alias_method_chain :find, :domaine
mouais ok mais bon comment fournir le bon domaine pour tous les Foo et Bar utilisés dans MyScript ?
Deuxième piste : une variable de classe mais attention il faut être sûr qu’elle soit valable uniquement pour le thread, voulant utiliser le hash Thread.current, j’ai quand même pris conseil chez coderr pour éviter d’abuser du Thread.current
class Class
def thread_local_accessor name, options = {}
m = Module.new
m.module_eval do
class_variable_set :"@@#{name}", Hash.new {|h,k| h[k] = options[:default] }
end
m.module_eval %{
FINALIZER = lambda {|id| @@#{name}.delete id }
def #{name}
@@#{name}[Thread.current.object_id]
end
def #{name}=(val)
ObjectSpace.define_finalizer Thread.current, FINALIZER unless @@#{name}.has_key? Thread.current.object_id
@@#{name}[Thread.current.object_id] = val
end
}
class_eval do
include m
extend m
end
end
end
et j’étends la classe active_record :
ActiveRecord::Base.class_eval do
thread_local_accessor :domain_store, :default => nil
class << self
def do_actions_with_domain(domain, &block)
ActiveRecord::Base.domain_store = domain
yield
ActiveRecord::Base.domain_store = nil
end
end
end
Ensuite j’appelle mon script encadré par do_actions_with_domain
ActiveRecord::Base.do_actions_with_domain('Economy') do
MyScript.new('foo_name', 'bar_name').do_a_lot_things
end
et voilà pas besoin de modifier ma class MyScript et ça marche au poil, merci Ruby