La magie de ruby …

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 🙂

Ce contenu a été publié dans Rails, Web Dev. Vous pouvez le mettre en favoris avec ce permalien.