Ruby style guide
15 May 2014
Voici maintenant quelques mois que je code en Ruby et je n’ai jamais pris le temps de centraliser les bonnes pratiques. Il est donc temps de m’y mettre !
Je me suis grandement inspiré de l’excellent The Ruby Style Guide.
Surtout, ne lisez pas la traduction française, elle n’est pas à jour, incomplète et parfois fausse !
Il existe plusieurs outils en ligne de commande pour vérifier le bon respect de ce guide mais, personnellement, j’utilise Rubocop.
Sachez aussi que vous pouvez écrire du code ruby plus “safe” grace à ruby -w
.
Encodage des fichiers
UTF-8 par défaut.
Les différentes versions de Ruby n’ont pas toujours utilisé le même encodage par défaut :
- ruby <= 1.8 n’avait pas de notion d’encodage. Les chaînes étaient plus ou moins des tableaux d’octets
- ruby 1.9 utilisait l’US_ASCII comme encodage par défaut. Il était possible de définir l’encodage en UTF-8 en ajoutant en haut de fichier :
# encoding: utf-8
- ruby >= 2.0 utilise l’UTF-8 par défaut
Espacement & Saut de ligne
Utilisez des espaces autour des opérateurs, après les :
, ;
et ,
, autour de {
et devant }
.
1
2
3
4
5
sum = 1 + 2
a, b = 1, 2
1 > 2 ? true : false; puts 'Hi'
[1, 2, 3].each { |e| puts e }
some(arg).other
L’opérateur d’exposant est la seule exception.
1
e = M * c**2
Pas d’espace après (
, [
ou devant ]
, )
.
Pas d’espace entre le nom d’une méthode et la parenthèse d’ouverture.
Pas d’espace après !
.
1
2
3
4
some(arg).other
[1, 2, 3].length
f(3 + 2) + 1
!something
Utilisez des espaces autour de =
lors de l’assignement des valeurs par défaut aux paramètres de méthode.
1
2
3
def some_method(arg1 = :default, arg2 = nil, arg3 = [])
# do something...
end
Sautez des lignes entre les def
.
1
2
3
4
5
6
7
def some_method
result
end
def some_method
result
end
Pensez à entourer le code interpolé d’un espace. Cela permet de mieux distinguer le code de la chaîne de caractères.
1
"#{ user.last_name }, #{ user.first_name }"
Identation & Alignement
Utilisez deux espaces par niveau d’indentation. Pas de tabulation.
Indentez when
au même niveau que case
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case
when song.name == 'Misty'
puts 'Not again!'
when song.duration > 120
puts 'Too long!'
when Time.now.hour > 21
puts "It's too late"
else
song.play
end
kind = case year
when 1850..1889 then 'Blues'
when 1890..1909 then 'Ragtime'
when 1910..1929 then 'New Orleans Jazz'
when 1930..1939 then 'Swing'
when 1940..1950 then 'Bebop'
else 'Jazz'
end
Alignez les paramètres de l’appel de méthode s’ils s’étalent sur plus d’une ligne.
1
2
3
4
5
6
def send_mail(source)
Mailer.deliver(to: 'bob@example.com',
from: 'us@example.com',
subject: 'Important message',
body: source.text)
end
Lisibilité
Utilisez RDoc et ses conventions pour la documentation d’API.
Limiter les lignes à 80 caractères.
Utiliser des fins de ligne de type Unix.
Ne laisser pas des espaces en fin de ligne.
Ajoutez des tirets bas aux grands expressions numériques.
1
num = 1_000_000
Évitez les prolongements de ligne \
. Sauf dans le cas de prolongement de chaine.
1
2
long_string = 'First part of the long string' \
' and second part of the long string'
N’utilisez pas de parenthèses autour des conditions d’un if
, unless
, while
, until
.
Utilisez ||=
pour initialiser des variables uniquement si elles ne le sont pas déjà.
Ne l’utilisez pas pour des variables booléennes.
1
2
name ||= 'Bozhidar'
enabled = true if enabled.nil?
Utilisez &&=
pour changer la valeur d’une variable qui existe déjà. Cela supprime l’intérêt de tester son existance avec if
.
1
something &&= something.downcase
Préfixez par _
les paramètres de bloc et les variables locales inutilisés.
1
2
3
4
5
6
result = hash.map { |_, v| v + 1 }
def something(x)
_, used_var = something_else(x)
# ...
end
Utilisez [*var]
ou Array()
, au lieu de d’une vérification explicite, lorsque vous voulez traiter une variable comme un Array
mais vous n’êtes pas sûr que c’est un Array
.
1
2
[*paths].each { |path| do_something(path) }
Array(paths).each { |path| do_something(path) }
Utilisez les plages ou Comparable#between?
au lieu d’une comparaison logique complexe.
1
2
do_something if (1000..2000).include?(x)
do_something if x.between?(1000, 2000)
Préférez les méthodes dédiées aux comparaisons explicites avec ==
. Sauf pour les numériques.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if x.even?
end
if x.odd?
end
if x.nil?
end
if x.zero?
end
if x == 0
end
Ne faites pas de vérification non-nil
sauf sur les valeurs booléennes.
1
2
3
4
5
do_something if something
def value_set?
!@some_boolean.nil?
end
Préférez :
map
àcollect
find
àdetect
select
àfind_all
reduce
àinject
size
àcount
oulenght
Privilégiez la notation littérale pour créer des tableaux ou des hashs (sauf si vous devez passer des paramètres à leurs constructeurs).
1
2
arr = []
hash = {}
Privilégiez %w
à la syntaxe littérale de création de tableau quand vous avez besoin d’un tableau de chaines de caractères.
1
STATES = %w(draft open closed)
Privilégiez %i
à la syntaxe littérale de création de tableau quand vous avez besoin d’un tableau de symboles.
1
STATES = %i(draft open closed)
Bonnes pratiques
Quand vous concevez des hiérarchies de classes, assurez-vous qu’elles sont conformes au Principe de substitution de Liskov et essayez de les rendre aussi SOLIDES que possible.
Utilisez ::
seulement pour référencer des constantes (de classes ou de modules) et des constructeurs (comme Array()
ou Nokogiri::HTML()
). Jamais pour invoquer des méthodes.
1
2
3
4
SomeClass.some_method
some_object.some_method
SomeModule::SomeClass::SOME_CONST
SomeModule::SomeClass()
Tirez parti du fait que if
et case
sont des expressions qui renvoient un résultat.
1
2
3
4
5
6
result =
if condition
x
else
y
end
N’utilisez jamais and
et or
. Ils sont bannis.
Utilisez toujours &&
et ||
.
N’utilisez jamais for
. Utiliser each
.
Evitez self
quand il n’est pas nécessaire. Il est nécessaire uniquement pour appeller un accesseur d’écriture local.
1
2
3
4
5
6
7
def ready?
if last_reviewed_at > last_updated_at
worker.update(content, options)
self.status = :in_progress
end
status == :verified
end
Utilisez $stdout/$stderr/$stdin
au lieu de STDOUT/STDERR/STDIN
.
Ce sont des constantes, vous pouvez les ré-assigner et vous aurez un avertissement de la part de l’interpréteur.
Utilisez warn
au lieu de $stderr.puts
.
Plus clair, plus concis. warn
vous permet également de supprimer les avertissements au besoin (en définissant le niveau d’avertissement à 0 via -W0
).
Préférez les modules aux classes qui comportent uniquement des méthodes. Les classes doivent être utilisé uniquement si il y a un intérêt de créer une instance de ces-dernières.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# bad
class SomeClass
def self.some_method
# body omitted
end
def self.some_other_method
end
end
# good
module SomeClass
module_function
def some_method
# body omitted
end
def some_other_method
end
end
Fournissez toujours une méthode to_s
qui représente vos objets.
1
2
3
4
5
6
7
8
9
10
11
12
class Person
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def to_s
"#{@first_name} #{@last_name}"
end
end
Privilégiez le duck-typing plutôt que l’héritage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# bad
class Animal
# abstract method
def speak
end
end
# extend superclass
class Duck < Animal
def speak
puts 'Quack! Quack'
end
end
# extend superclass
class Dog < Animal
def speak
puts 'Bau! Bau!'
end
end
# good
class Duck
def speak
puts 'Quack! Quack'
end
end
class Dog
def speak
puts 'Bau! Bau!'
end
end
Utilisez def self.method
pour définir les méthodes statiques.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestClass
def self.some_other_method
# body omitted
end
# Also possible and convenient when you
# have to define many singleton methods.
class << self
def first_method
# body omitted
end
def second_method_etc
# body omitted
end
end
end
Utilisez Set
au lieu de Array
quand vous travaillez sur des éléments uniques.
Set
implémente une collection de valeurs uniques désordonnées. C’est un mélange entre les fonctionnalités pratiques et intuitives d’un Array
et l’accès rapide dans un Hash
.
Préférez les symboles aux chaines pour les clés dans un Hash
.
1
hash = { one: 1, two: 2, three: 3 }
Utilisez Hash#fetch
quand vous souhaitez récupérer une clé présente dans un Hash
.
1
2
3
4
5
6
7
heroes = { batman: 'Bruce Wayne', superman: 'Clark Kent' }
# bad - if we make a mistake we might not spot it right away
heroes[:batman] # => "Bruce Wayne"
heroes[:supermann] # => nil
# good - fetch raises a KeyError making the problem obvious
heroes.fetch(:supermann)
Jouez avec Hash#fetch
pour définir une valeur par défaut lorsque la clé n’existe pas dans un Hash
.
1
2
3
4
5
6
7
batman = { name: 'Bruce Wayne', is_evil: false }
# bad - if we just use || operator with falsy value we won't get the expected result
batman[:is_evil] || true # => true
# good - fetch work correctly with falsy values
batman.fetch(:is_evil, true) # => false
Si vous avez besoin de transiter par une méthode pour récupérer la valeur par défaut, utilisez un block. La méthode sera appelée uniquement si besoin.
1
batman.fetch(:powers) { get_batman_powers }
Evitez d’utiliser String#+
pour assembler de longues chaînes de caractères.
Utilisez plutôt String#<<
qui transforme la chaîne sur place et s’avère toujours plus rapide que String#+
, qui crée beaucoup de nouveaux objets.
1
2
3
4
5
6
html = ''
html << '<h1>Page title</h1>'
paragraphs.each do |paragraph|
html << "<p>#{paragraph}</p>"
end
Nommage
Utilisez le snake_case
pour :
- les symboles, les méthodes et les variables
- les noms de fichier (ex:
hello_world.rb
) - les noms de répertoire (ex:
lib/hello_world/hello_world.rb
)
1
2
3
4
5
6
7
:some_symbol
some_var = 5
def some_method
...
end
Utilisez le CamelCase
pour les classes et les modules.
Conservez les acronymes comme HTTP, RFC, XML en uppercase
.
1
2
3
4
5
6
7
class SomeClass
...
end
class SomeXML
...
end
Essayez d’avoir une classe ou un module par fichier. Nommez ce fichier avec le nom de la classe ou du module, en remplacant le CamelCase
par du snake_case
.
Utilisez le SCREAMING_SNAKE_CASE
pour les constantes.
1
SOME_CONST = 5
Seulement les méthodes qui retournent une valeur booléenne doivent se finir par ?
.
1
["foo", "bar"].empty?
Les méthodes dangereuses (qui modifient self
ou les arguments, …) doivent se finir par !
. Une version plus sûr de la méthode dangereuse doit toujours exister.
1
2
3
4
5
6
7
class Person
def update!
end
def update
end
end
Commentaires
Un code de qualité constitue sa propre documentation. Quand vous êtes sur le point d’ajouter un commentaire, demandez-vous, “Comment puis-je améliorer le code pour que ce commentaire ne soit pas nécessaire ?” Améliorez le code et documentez le ensuite pour le rendre encore plus limpide. Steve McConnell
Utilisez :
TODO
pour marquer les fonctionnalités manquantes ou qui devraient être ajoutées ultérieurementFIXME
pour signaler un code erroné qui doit être corrigéOPTIMIZE
pour signaler un code lent ou inefficace pouvant poser des problèmes de performanceHACK
pour signaler un code qui semble être issu de pratiques d’écriture douteuses qui devrait être refactoriséREVIEW
pour signaler toute chose devant être vérifiée pour confirmer qu’elle fonctionne comme prévu. Par exemple:REVIEW: Are we sure this is how the client does X currently?
- d’autres mots clés d’annotation personnalisés si vous considérez que c’est approprié, mais assurez-vous de les documenter dans le README de votre projet.
Exceptions
Signalez les exceptions en utilisant la méthode fail
.
Utilisez raise
uniquement lorsque vous capturez une exception et la levez à nouveau (parcequ’il ne s’agit pas d’un échec mais d’une levée d’exception explicite et intentionnelle).
1
2
3
4
5
begin
fail 'Oops'
rescue => error
raise if error.message != 'Oops'
end
N’utilisez jamais return
dans un bloc ensure
. Cela donnerait la priorité au retour sur une éventuelle levée d’exception, et la méthode retournerait un résultat comme si aucune exception n’avait été levée. En fait, l’exception serait ignorée silencieusement.
1
2
3
4
5
6
7
def foo
begin
fail
ensure
return 'very bad idea'
end
end
Utilisez des blocs begin
implicites autant que possible.
1
2
3
4
5
def foo
# main logic goes here
rescue
# failure handling goes here
end
Limitez la prolifération des blocs begin
en utilisant les méthodes de contingence (un terme inventé par Avdi Grimm.)
1
2
3
4
5
6
7
8
def with_io_error_handling
yield
rescue IOError
# handle IOError
end
with_io_error_handling { something_that_might_fail }
with_io_error_handling { something_else_that_might_fail }
Evitez rescue Exception
.
Cela aurait pour effet d’intercepter les signaux et appels à exit
, et nécesiterait de kill -9
le processus.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# bad
begin
# calls to exit and kill signals will be caught (except kill -9)
exit
rescue Exception
puts "you didn't really want to exit, right?"
# exception handling
end
# good
begin
# a blind rescue rescues from StandardError, not Exception as many
# programmers assume.
rescue => e
# exception handling
end
# also good
begin
# an exception occurs here
rescue StandardError => e
# exception handling
end
Libérez les ressources externes ouvertes par votre programme dans un bloc ensure
.
1
2
3
4
5
6
7
8
f = File.open('testfile')
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
Privilégiez l’utilisation d’exceptions standards plutôt que d’introduire de nouvelles classes d’exception.