Comment tirer parti d'Active Support

Publié le 28 avril 2011 par Nicolas Cavigneaux | back

Cet article est publié sous licence CC BY-NC-SA

Active Support, c’est quoi ?

Active Support est un composant inclut dans rails de manière totalement transparente. Même si vous ne connaissez pas son existence ou ses possibilités, vous l’avez très certainement déjà utilisé. Vous connaissez "string".blank? ou 1.year.ago alors vous avez déjà utilisé ActiveSupport !

Active Support a pour but de fournir un certain nombre d’extensions et d’utilitaires à la librairie standard Ruby. Le gros avantage d’Active Support est qu’il est extrêmement modulaire et très léger en terme d’emprunte mémoire. De plus, il est vraiment facile de l’intégrer à un projet Ruby n’ayant rien à voir avec Rails.

Il faut savoir qu’Active Support était d’abord conçu pour faciliter le développement de Rails à proprement parler. Il a, dans la foulée, était intégré à l’API publique de Rails pour que les développeurs puissent en tirer parti. Tout naturellement, au fil du temps de plus en plus de projet non-Rails ont commencés à intégrer Active Support pour simplifier le développement.

Malgré ces qualités indéniables, Active Support reste largement sous-exploité à mon goût dans les projets non-Rails mais même dans les applications Web écrites en Rails…

Il n’est pas rare d’étudier une application écrite en Ruby on Rails dans laquelle on a l’impression que le développeur a ré-inventé la roue. Comme si certains développeurs n’avaient tout simplement pas connaissance de l’existence d’Active Support.

Cet article va donc nous permettre de faire un tour rapide des possibilités d’Active Support ce qui nous permettra d’éviter de ré-écrire des routines existantes et testées.

Installation

Si vous êtes dans un projet Rails alors vous avez déjà accès à Active Support de façon tout à fait transparente.

Si vous souhaitez intégrer Active Support à un projet non-Rails, les étapes sont d’une simplicité enfantine. Tout d’abord il faut avoir installé le gem “activesupport”, par exemple avec un gem install activesupport mais en bon développeur que vous êtes votre projet contient certainement un gemfile. Il suffit d’ajouter ce gem pour ensuite lancer un bundle install.

Chargement d’Active Support

Pour pouvoir garder une empreinte mémoire faible, Active Support ne charge rien par défaut. Tout le code est découpé en petites parties ce qui permet de charger uniquement ce dont on a besoin.

Si vous faites un require 'active_support' vous remarquerez qu’aucune différence n’est visible.

require "rubygems"
# => true
require "active_support"
# => true
"test".blank?
# NoMethodError: undefined method `blank?' for "test":String

Vos objets n’ont toujours pas hérité des possibilités d’Active Support. Vous pouvez charger toutes les extensions : require "active_support/all"

Vous aurez dès lors accès à toutes les extensions fournies par Active Support. Il faut tout de même noter qu’en pratique toutes les librairies ne seront pas chargées. Certaines ne seront chargées qu’au premier appel.

Bien que la solution de tout charger d’un coup puisse paraître pratique, dans les faits, la majorité des projets qui utilisent Active Support ne chargent que les parties dont ils ont besoin. D’un point de vue architecture et performance, c’est la solution qui me parait la plus appropriée.

Supposons que vous ayez besoin de la méthode blank?, vous pouvez alors simplement charger le fichier qui définit cette méthode : require 'active_support/core_ext/object/blank'. Le chargement des dépendances est bien évidemment géré de manière automatique.

Vous pouvez également charger un groupe de méthodes, par exemple, toutes les extensions disponibles pour la classe Object avec un require 'active_support/core_ext/object'.

Extensions disponibles

Active Support amène avec lui de très nombreuses méthodes qui peuvent s’avérer bien pratiques. Ces méthodes sont regroupées par catégories. Nous n’allons pas essayer d’en faire le tour dans cet article mais je vous conseille de jeter un œil au code ou à la RDoc qui vous permettront d’avoir une vision compléte des possibilités et des situations dans lesquelles les utiliser.

‘active_support/core_ext/’ est de loin le répertoire le plus fourni. Son but est de parfaire la librairie standard de Ruby en y ajoutant tout un tas de méthodes qui simplifient la vie. Les autres répertoires cachent aussi parfois des choses intéressantes.

Voici quelques exemples des choses qui s’avèrent les plus utiles.

Manipulation de chaines

blank? : permet de vérifier l’état d’un objet

"  ".blank? # => true
"".blank?   # => true
nil.blank?  # => true
[].blank?   # => true

Tout les objets répondants à la méthode empty? peuvent utiliser la méthode blank?.

On peut également utiliser la méthode present? qui est l’exact inverse ce qui permet par exemple de vérifier la présence d’un paramètre donné dans un formulaire.

La méthode presence est un dérivé qui va vous permettre de retourner la valeur du receveur s’il y en a une ou nil si ce n’est pas le cas. Cette méthode est parfaite pour utiliser des idiomes du type :

name = params[:name].presence || 'Anonymous'

qui nous évite un fastidieux :

name = params[:name].present? ? params[:name] : 'Anonymous'

Si vous devez manipuler des chaines pour créer des uri valides, des noms de classes, etc. Tout un éventail de méthodes vous est fournit :

"active_record".camelize          # => "ActiveRecord"
"active_record/errors".camelize   # => "ActiveRecord::Errors"

"posts".classify                  # => "Post"

"Module".constantize              # => Module

"nico_synbioz".dasherize          # => "nico-synbioz"

"Synbioz::Ext".demodulize         # => "Ext"

"Lorem Ipsum".parameterize        # => "lorem-ipsum"

"website".pluralize               # => "websites"

"websites".singularize            # => "website"

"nicolas cavigneaux".titleize     # => "Nicolas Cavigneaux"

"àbrâcaddabrä".transliterate      # => "abracaddabra"

ActiveRecord".underscore          # => "active_record"

Certaines de ces méthodes prennent des paramètres qui modifient leurs comportements. La documentation vous apprendra tout.

Il est toujours plus sympa de pouvoir faire attribut.valeur? plutôt que attribut == "valeur". C’est ce que permet StringInquirer :

class Klass
  def initialize(title)
    @title = title
  end

  def title
    ActiveSupport::StringInquirer.new(@title)
  end
end

k = Klass.new("synbioz")
k.title.microsoft? # => false
k.title.synbioz?   # => true

Si vous avez besoin de compresser / décompresser des chaines :

compressed = ActiveSupport::Gzip.compress("test")
ActiveSupport::Gzip.decompress(compressed) # => "test"

Il se peut que parfois vous deviez stocker des données qui ne doivent ni filtrer, ni être manipulées disons dans un cookie utilisateur.

secret = "une chaine longue, compliquée et aléatoire"
encryptor = ActiveSupport::MessageEncryptor.new(secret)

cipher = encryptor.encrypt("message secret que l'on va stocker dans un cookie")
encryptor.decrypt(cipher) # => "message secret que l'on va stocker dans un cookie"

Vous devez générer une chaine aléatoire :

SecureRandom.hex(10) # => "52750b30ffbc7de3b362"
SecureRandom.hex(10) # => "92b15d6c8dc4beb5f559"
SecureRandom.base64(10) # => "EcmTPZwWRAozdA=="
SecureRandom.base64(10) # => "9b0nsevdwNuM/w=="

Gestion des dates simplifiée

Le module ActiveSupport::Duration améliore de façon considérable le travail sur les dates.

Date.today + 1.year
Date.today - 1.year

2.month.ago   # => Date 2 mois dans le passé
2.month.since # => Date 2 mois dans le futur

La classe Date est d’ailleurs elle même bien fournie :

Date.today.beginnig_of_day
Date.today.beginnig_of_week
Date.today.beginnig_of_month
Date.today.beginnig_of_year

Date.today.end_of_day
Date.today.end_of_week
Date.today.end_of_month
Date.today.end_of_year

Date.today.change(:year => 2020)

Date.today.today?
Date.today.future?
Date.today.past?

Enumerable gonflé à bloc

[1, 2].include? 2 # => true
[1, 2].exclude? 3 # => true

people.group_by(&:lastname).each { |lastname, family_people|  }

people.index_by(&:login) # => {:nico => #Person, :martin => #Person}

[1].any?  # => true
[1].many? # => false

%w( a b c d ).from(2)  # => %w( c d )
%w( a b c d ).from(10) # => nil

%w( a b c d ).to(2)  # => %w( a b c )
%w( a b c d ).to(10) # => %w( a b c d )

%w(1 2 3).in_groups(2) {|group| p group}
# ["1", "2"]
# ["3", nil]
# => [["1", "2"], ["3", nil]]

%w(1 2 3).in_groups(2, false) {|group| p group}
# ["1", "2"]
# ["3"]
# => [["1", "2"], ["3"]]

w(1 2 3).in_groups_of(2) {|group| p group}
# ["1", "2"]
# ["3"]
# => nil

[1,2,3,4,5,6].sample    # => 4
[1,2,3,4,5,6].sample(3) # => [2, 4, 5]

%w(Tom Jerry).to_sentence          # => "Tom and Jerry"
%w(Atos Portos Aramis).to_sentence # => "Atos, Portos and Aramis"

[1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2]

Et la méthode dont vous comprendrez tout l’intérêt sans mon aide. Une révélation de DHH… :

[...].forty_two # Retourne le 42éme élément du tableau

Simplifier vos classes

Il arrive souvent d’avoir des méthodes qui doivent faire de lourds calculs. memoize permet de cacher le résultat de ces méthodes gourmandes en temps de calcul. Cette méthode à l’avantage de savoir gérer des méthodes attendants des paramètres et donc de cacher le résultat associé à un argument donné. Si vous avez l’habitude d’utiliser ||= alors memoize va vous plaire :

def methode_gourmande(arg1, arg2)
  # Calcul
end

memoize :methode_gourmande

Supposons que vous vouliez enrichir une méthode existante mais sans l’écraser, alias_method_chain est dédié à ce besoin plutôt récurrent :

# Sans alias_method_chain
alias_method :method_without_feature, :method
alias_method :method, :method_with_feature

# Avec alias_method_chain
alias_method_chain :method, :feature

Vous avez nommé l’une de vos colonne “title” mais vous souhaitez aussi pouvoir y faire référence sous le nom “subject”, c’est ce que propose alias_attribute en mettant en place pour vous des alias pour les attributs (getter, setter, …) :

alias_attribute :title, :subject

J’ai assez régulièrement besoin d’un attribut qui renvoie une valeur par défaut quand l’attribut n’a pas été renseigné, c’est le but de attr_accessor_with_default :

attr_accessor_with_default(:address) {
  [city, zipcode, country].compact.join(", ")
}

Fonctionnalités transversales

Une autre méthode que j’utilise très souvent pour m’épargner des tests est try. Parfois on souhaite afficher une propriété d’un objet sans savoir à l’avance si cet objet existe ou non. try va essayer d’appeler la méthode sur le receveur et renvoyer la valeur retournée comme le ferait send. Toutefois send lancerait une exception si le receveur est nul ou ne contient pas la dite propriété. Dans ce cas try renverra nil plutôt qu’une exception.

# Sans try
@person ? @person.name : nil

# Avec try
@person.try(:name)

benchmark permet de mesurer le temps d’exécution d’un bloc de code. Pratique pour savoir ce qui ralentit le rendu d’une vue par exemple.

<% benchmark "Génération du tableau" do %>
  <%= generate_big_table %>
<% end %>

Conclusion

Cet article vous aura peut-être fait découvrir des méthodes utiles au quotidien ou donné envie d’utiliser Active Support dans vos projets pour simplifier votre code. Cette article n’est pas une invitation à l’abus, Active Support peut se révéler très pratique nais ne justifie pas à lui seul qu’on l’inclut dans sa totalité là où une seule méthode sera utilisée.

Dernière chose concernant Active Support, il est facile de trouver des méthodes qui peuvent sembler utiles mais qui toutefois ne sont pas intégrées. Vous pouvez toujours essayer de proposer un patch mais il y a très peu de chance que ce dernier soit intégré à moins qu’il réponde à un cas d’utilisation extrêmement courant.

Si vous souhaitez réellement participer à un projet open-source qui acceptera plus facilement vos propositions d’ajouts et vous permettra par la même occasion de diffuser largement vos ajouts à l’API standard de Ruby, je vous conseille vivement de vous intéresser à Facets.

L’équipe Synbioz.

Libres d’être ensemble.