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 🙂