Configuration applicative
Lorsqu’il s’agit de configurer une application Rails, chez Synbioz, on aime
bien y apporter une grande souplesse pour pouvoir nous adapter à de multiples
situations. C’est pourquoi on favorise l’usage de variables d’environnement.
Pour accéder à ces variables, on pourra utiliser ENV.fetch("ma_variable")
si
sa présence est obligatoire, ou ENV["ma_variable"]
si elle est optionnelle.
Se pose alors la question des variables booléennes. Par convention, nous avons
choisi de favoriser “0” ou “1” au détriment d’autres valeurs comme “true”,
“FALSE”, “yes”, “f”, etc. Ainsi, une variable d’environnement booléenne sera
récupérée via ENV.fetch("ma_variable").to_i.positive?
.
Fail fast
The most annoying aspect of software development, for me, is debugging. I don’t mind the kinds of bugs that yield to a few minutes’ inspection. The bugs I hate are the ones that show up only after hours of successful operation, under unusual circumstances, or whose stack traces lead to dead ends. Fortunately, there’s a simple technique that will dramatically reduce the number of these bugs in your software. It won’t reduce the overall number of bugs, at least not at first, but it’ll make most defects much easier to find. The technique is to build your software to “fail fast.”
— Jim Shore
Dans l’idéal, quel que soit le framework ou le langage, il est préférable de récupérer l’ensemble des variables d’environnement utiles à l’application au démarrage de celle-ci, de manière centralisée pour faciliter la prise de connaissance de ces variables et leur mise à jour. Ainsi, si une variable est manquante au démarrage, on pourra faire planter l’application dès son lancement avec un message explicite. Ceci évite d’avoir des plantages aléatoires à l’exécution ; à l’envoi d’un courriel ou lors d’un appel à une API par exemple.
Configuration X
Dans le cas d’une application Rails, on va centraliser la récupération des
variables d’environnement dans le fichier config/application.rb
. On a donc
notre point centralisé, chargé au démarrage de l’application qui va nous
permettre d’être robuste face aux variables d’environnement manquantes.
Rails prévoit un mécanisme pour stocker toutes les informations de configuration
transversales à l’application. Cela nous évite de passer par un système maison,
ou pire, des variables globales. Rails.configuration.x
permet de stocker
l’ensemble des données de configuration pour une instance donnée et de récupérer
très facilement ces infos depuis n’importe où dans l’application.
L’implémentation de Rails.configuration.x
mérite qu’on s’y attarde ! Il s’agit
d’une instance de la classe Custom
déclarée comme ceci :
# railties/lib/rails/application/configuration.rb
module Rails
class Application
class Configuration < ::Rails::Engine::Configuration
def initialize(*)
@x = Custom.new
end
class Custom #:nodoc:
def initialize
@configurations = Hash.new
end
def method_missing(method, *args)
if method.end_with?("=")
@configurations[:"#{method[0..-2]}"] = args.first
else
@configurations.fetch(method) {
@configurations[method] = ActiveSupport::OrderedOptions.new
}
end
end
def respond_to_missing?(symbol, *)
true
end
end
end
end
end
On observe que la technique consiste à faire usage de la méthode
method_missing
, nous offrant ainsi la possibilité de récupérer ou d’affecter
une valeur via n’importe quelle méthode de notre choix sur cet objet. On
remarque que si la clé foo
n’existe pas dans le dictionnaire @configurations
,
c’est-à-dire la première fois qu’on fait appel à Rails.configuration.foo
, une
nouvelle instance d’ActiveSupport::OrderedOptions.new
est créée. Il s’agit
d’une classe qui hérite de la classe Hash
et qui fournit des accesseurs
dynamiques.
Avec un Hash
, les paires clé-valeur sont généralement manipulées comme ceci :
h = {}
h[:boy] = 'John'
h[:girl] = 'Mary'
h[:boy] # => 'John'
h[:girl] # => 'Mary'
h[:dog] # => nil
En utilisant un OrderedOptions
, l’exemple ci-dessus peut être écrit comme
ceci :
h = ActiveSupport::OrderedOptions.new
h.boy = 'John'
h.girl = 'Mary'
h.boy # => 'John'
h.girl # => 'Mary'
h.dog # => nil
Il est aussi possible de lever une exception si la valeur est manquante :
h.dog! # => raises KeyError: :dog is blank
Dans ce contexte, l’utilisation conjointe de method_missing
et
OrderedOptions
nous offre une grande souplesse à l’usage. C’est une approche
intéressante, notamment dans le cas d’un framework ou d’une bibliothèque
généraliste, mais coûteuse et déconseillée pour implémenter un code métier aux
règles de gestion bien connues et maîtrisées.
Remarquons ici une bonne pratique souvent oubliée lorsqu’on fait usage de
method_missing
: implémenter également respond_to_missing?
de manière à
indiquer si la méthode que l’on s’apprête à utiliser est implémentée ou non à la
volée par method_missing
. Dans notre cas, on répondra toujours oui (true
)
parce que notre implémentation de method_missing
se comportera toujours comme
un accesseur, peu importe le nom de la méthode qu’on lui passe en argument.
À l’usage
Dans les faits, en suivant les recommandations précédentes, nous pourrions nous retrouver avec une configuration applicative qui ressemble à ceci :
# config/application.rb
module MyApp
class Application < Rails::Application
# …
config.x.api_url = ENV.fetch("API_URL")
config.x.api_scheme = ENV.fetch("API_SCHEME", "http")
config.x.enable_foo = ENV.fetch("ENABLE_FOO", 0).to_i.positive?
# …
end
end
Et l’utiliser de cette manière dans notre application :
Rails.configuration.x.api_url
Rails.configuration.x.enable_foo == true
Allons un peu plus loin
Rails nous offre un outil supplémentaire qui peut s’avérer fort utile, j’ai
nommé config_for
. Il s’agit d’un moyen de charger une configuration
applicative à partir d’un fichier YAML. Cerise sur le gâteau, l’environnement
courant de Rails est pris en compte ! Voici un petit exemple :
# config/api_custom.yml
defaults: &defaults
timeout: <%= ENV.fetch("API_CUSTOM_TIMEOUT", 20).to_i %>
development:
<<: *defaults
url: <%= ENV.fetch("API_CUSTOM_URL", "https://custom-dev.api.example.org/api/v2") %>
test:
<<: *defaults
url: https://custom-test.api.custom.org/api/v2
production:
<<: *defaults
url: <%= ENV.fetch("API_CUSTOM_URL", "https://custom.api.custom.org/api/v2") %>
# config/application.rb
class Application < Rails::Application
# Custom Configuration
config.x.api_custom = config_for(:api_custom)
end
À présent, nous pouvons faire appel à notre configuration :
Rails.configuration.api_custom.timeout
Rails.configuration.api_custom.url
Avouez que c’est bien pratique ! Ainsi notre configuration applicative est à la fois centralisée et contextualisée ; fini les variables de configuration obscures qui surgissent d’on ne sait où !