Par Bojidar Batsov
Les modèles de rôle sont importants.
— Officier Alex J. Murphy / RoboCop
Conseil | Vous pouvez trouver une belle version de ce guide avec une navigation bien améliorée sur https://rails.rubystyle.guide. |
L'objectif de ce guide est de présenter un ensemble de bonnes pratiques et de prescriptions de style pour le développement de Ruby on Rails. Il s'agit d'un guide complémentaire au guide de style de codage Ruby déjà existant, piloté par la communauté.
Ce guide de style Rails recommande les meilleures pratiques afin que les programmeurs Rails du monde réel puissent écrire du code qui peut être maintenu par d'autres programmeurs Rails du monde réel. Un guide de style qui reflète une utilisation réelle est utilisé, et un guide de style qui s'en tient à un idéal qui a été rejeté par les personnes qu'il est censé aider risque de ne pas être utilisé du tout, aussi bon soit-il.
Le guide est séparé en plusieurs sections de règles connexes. J'ai essayé d'ajouter la justification des règles (si elle est omise, j'ai supposé que c'était assez évident).
Je n'ai pas trouvé toutes les règles de nulle part - elles sont principalement basées sur ma longue carrière d'ingénieur logiciel professionnel, les commentaires et suggestions des membres de la communauté Rails et diverses ressources de programmation Rails très appréciées.
Note | Certains des conseils présentés ici ne s'appliquent qu'aux versions récentes de Rails. |
Vous pouvez générer une copie PDF de ce guide à l'aide d'AsciiDoctor PDF et une copie HTML avec AsciiDoctor à l'aide des commandes suivantes :
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Conseil | Installez la gemme gem install rouge |
Les traductions du guide sont disponibles dans les langues suivantes :
japonais
russe
Conseil | RuboCop, un analyseur de code statique (linter) et formateur, possède une extension rubocop-rails , basée sur ce guide de style. |
Mettez le code d'initialisation personnalisé dans config/initializers
. Le code des initialiseurs s'exécute au démarrage de l'application.
Conservez le code d'initialisation de chaque gem dans un fichier séparé portant le même nom que la gem, par exemple carrierwave.rb
, active_admin.rb
, etc.
Ajustez en conséquence les paramètres de l'environnement de développement, de test et de production (dans les fichiers correspondants sous config/environments/
)
Marquez les ressources supplémentaires pour la précompilation (le cas échéant) :
# config/environments/production.rb
# Precompile additional assets (application.js, application.css,
#and all non-JS/CSS are already added)
config . assets . precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
Conservez la configuration applicable à tous les environnements dans le fichier config/application.rb
.
Lors de la mise à niveau vers une version plus récente de Rails, le paramètre de configuration de votre application restera sur la version précédente. Pour profiter des dernières pratiques Rails recommandées, le paramètre config.load_defaults
doit correspondre à votre version Rails.
# good
config . load_defaults 6.1
Évitez de créer des configurations d'environnement supplémentaires par rapport aux valeurs par défaut de development
, test
et production
. Si vous avez besoin d'un environnement de production tel qu'un environnement intermédiaire, utilisez des variables d'environnement pour les options de configuration.
Conservez toute configuration supplémentaire dans les fichiers YAML sous le répertoire config/
.
Depuis Rails 4.2, les fichiers de configuration YAML peuvent être facilement chargés avec la nouvelle méthode config_for
:
Rails :: Application . config_for ( :yaml_file )
Lorsque vous devez ajouter plus d'actions à une ressource RESTful (en avez-vous vraiment besoin ?), utilisez les routes member
et collection
.
# bad
get 'subscriptions/:id/unsubscribe'
resources :subscriptions
# good
resources :subscriptions do
get 'unsubscribe' , on : :member
end
# bad
get 'photos/search'
resources :photos
# good
resources :photos do
get 'search' , on : :collection
end
Si vous devez définir plusieurs itinéraires member/collection
utilisez la syntaxe de bloc alternative.
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
Utilisez des itinéraires imbriqués pour mieux exprimer la relation entre les modèles Active Record.
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
Si vous avez besoin d'imbriquer des itinéraires à plus d'un niveau de profondeur, utilisez l'option shallow: true
. Cela évitera à l'utilisateur les URL longues posts/1/comments/5/versions/7/edit
et vous des assistants d'URL longues edit_post_comment_version
.
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
Utilisez des itinéraires avec espace de noms pour regrouper les actions associées.
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
N’utilisez jamais l’ancienne route du contrôleur sauvage. Cette route rendra toutes les actions de chaque contrôleur accessibles via des requêtes GET.
# very bad
match ':controller(/:action(/:id(.:format)))'
N'utilisez pas match
pour définir des routes, sauf s'il est nécessaire de mapper plusieurs types de requêtes parmi [:get, :post, :patch, :put, :delete]
à une seule action à l'aide de l'option :via
.
Gardez les contrôleurs maigres - ils ne doivent récupérer que les données pour la couche de vue et ne doivent contenir aucune logique métier (toute la logique métier doit naturellement résider dans le modèle).
Chaque action du contrôleur devrait (idéalement) invoquer une seule méthode autre qu'une recherche initiale ou une nouvelle.
Réduisez le nombre de variables d’instance transmises entre un contrôleur et une vue.
Les actions du contrôleur spécifiées dans l’option Filtre d’action doivent avoir une portée lexicale. Le ActionFilter spécifié pour une action héritée rend difficile la compréhension de la portée de son impact sur cette action.
# bad
class UsersController < ApplicationController
before_action :require_login , only : :export
end
# good
class UsersController < ApplicationController
before_action :require_login , only : :export
def export
end
end
Préférez utiliser un modèle plutôt que le rendu en ligne.
# very bad
class ProductsController < ApplicationController
def index
render inline : "<% products.each do |p| %><p><%= p.name %></p><% end %>" , type : :erb
end
end
# good
## app/views/products/index.html.erb
<%= render partial : 'product' , collection : products %>
## app/views/products/_product.html.erb
< p ><%= product . name %>< /p>
<p><%= product.price %></p >
## app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
render :index
end
end
Préférez render plain:
au render text:
.
# bad - sets MIME type to `text/html`
...
render text : 'Ruby!'
...
# bad - requires explicit MIME type declaration
...
render text : 'Ruby!' , content_type : 'text/plain'
...
# good - short and precise
...
render plain : 'Ruby!'
...
Préférez les symboles correspondants aux codes d’état HTTP numériques. Ils sont significatifs et ne ressemblent pas à des nombres « magiques » pour les codes d’état HTTP moins connus.
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
Introduisez librement des classes de modèles non-Active Record.
Nommez les modèles avec des noms significatifs (mais courts) sans abréviations.
Si vous avez besoin d'objets prenant en charge un comportement de type ActiveRecord (comme les validations) sans la fonctionnalité de base de données, utilisez ActiveModel::Model
.
class Message
include ActiveModel :: Model
attr_accessor :name , :email , :content , :priority
validates :name , presence : true
validates :email , format : { with : / A [-a-z0-9_+ . ]+ @ ([-a-z0-9]+ . )+[a-z0-9]{2,4} z /i }
validates :content , length : { maximum : 500 }
end
À partir de Rails 6.1, vous pouvez également étendre l'API d'attributs d'ActiveRecord en utilisant ActiveModel::Attributes
.
class Message
include ActiveModel :: Model
include ActiveModel :: Attributes
attribute :name , :string
attribute :email , :string
attribute :content , :string
attribute :priority , :integer
validates :name , presence : true
validates :email , format : { with : / A [-a-z0-9_+ . ]+ @ ([-a-z0-9]+ . )+[a-z0-9]{2,4} z /i }
validates :content , length : { maximum : 500 }
end
À moins qu'elles n'aient une signification dans le domaine métier, n'insérez pas dans votre modèle de méthodes qui formatent simplement vos données (comme du code générant du HTML). Ces méthodes seront très probablement appelées à partir de la couche d'affichage uniquement, leur place est donc dans les assistants. Conservez vos modèles uniquement pour la logique métier et la persistance des données.
Évitez de modifier les valeurs par défaut d'Active Record (noms de table, clé primaire, etc.) sauf si vous avez une très bonne raison (comme une base de données qui n'est pas sous votre contrôle).
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
Évitez de définir ignored_columns
. Cela peut écraser les affectations précédentes et c'est presque toujours une erreur. Préférez plutôt ajouter à la liste.
class Transaction < ApplicationRecord
# bad - it may overwrite previous assignments
self . ignored_columns = %i[ legacy ]
# good - the value is appended to the list
self . ignored_columns += %i[ legacy ]
...
end
Préférez utiliser la syntaxe de hachage pour enum
. Array rend les valeurs de la base de données implicites et toute insertion/suppression/réarrangement de valeurs au milieu entraînera très probablement un code cassé.
class Transaction < ApplicationRecord
# bad - implicit values - ordering matters
enum type : %i[ credit debit ]
# good - explicit values - ordering does not matter
enum type : {
credit : 0 ,
debit : 1
}
end
Regroupez les méthodes de style macro ( has_many
, validates
, etc.) au début de la définition de la classe.
class User < ApplicationRecord
# keep the default scope first (if any)
default_scope { where ( active : true ) }
# constants come up next
COLORS = %w( red green blue )
# afterwards we put attr related macros
attr_accessor :formatted_date_of_birth
attr_accessible :login , :first_name , :last_name , :email , :password
# Rails 4+ enums after attr macros
enum role : { user : 0 , moderator : 1 , admin : 2 }
# followed by association macros
belongs_to :country
has_many :authentications , dependent : :destroy
# and validation macros
validates :email , presence : true
validates :username , presence : true
validates :username , uniqueness : { case_sensitive : false }
validates :username , format : { with : / A [A-Za-z][A-Za-z0-9._-]{2,19} z / }
validates :password , format : { with : / A S {8,128} z / , allow_nil : true }
# next we have callbacks
before_save :cook
before_save :update_username_lower
# other macros (like devise's) should be placed after the callbacks
...
end
has_many :through
Préférez has_many :through
à has_and_belongs_to_many
. L'utilisation has_many :through
permet des attributs et des validations supplémentaires sur le modèle de jointure.
# not so good - using has_and_belongs_to_many
class User < ApplicationRecord
has_and_belongs_to_many :groups
end
class Group < ApplicationRecord
has_and_belongs_to_many :users
end
# preferred way - using has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups , through : :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
class Group < ApplicationRecord
has_many :memberships
has_many :users , through : :memberships
end
Préférez self[:attribute]
à read_attribute(:attribute)
.
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
Préférez self[:attribute] = value
à write_attribute(:attribute, value)
.
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
Utilisez toujours les validations « nouveau style ».
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
Lorsque vous nommez des méthodes de validation personnalisées, respectez les règles simples :
validate :method_name
se lit comme une déclaration naturelle
le nom de la méthode explique ce qu'elle vérifie
la méthode est reconnaissable comme méthode de validation par son nom, et non comme méthode de prédicat
# good
validate :expiration_date_cannot_be_in_the_past
validate :discount_cannot_be_greater_than_total_value
validate :ensure_same_topic_is_chosen
# also good - explicit prefix
validate :validate_birthday_in_past
validate :validate_sufficient_quantity
validate :must_have_owner_with_no_other_items
validate :must_have_shipping_units
# bad
validate :birthday_in_past
validate :owner_has_no_other_items
Pour rendre les validations faciles à lire, ne répertoriez pas plusieurs attributs par validation.
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
Lorsqu'une validation personnalisée est utilisée plusieurs fois ou que la validation est un mappage d'expression régulière, créez un fichier de validateur personnalisé.
# bad
class Person
validates :email , format : { with : / A ([^@ s ]+)@((?:[-a-z0-9]+ . )+[a-z]{2,}) z /i }
end
# good
class EmailValidator < ActiveModel :: EachValidator
def validate_each ( record , attribute , value )
record . errors [ attribute ] << ( options [ :message ] || 'is not a valid email' ) unless value =~ / A ([^@ s ]+)@((?:[-a-z0-9]+ . )+[a-z]{2,}) z /i
end
end
class Person
validates :email , email : true
end
Conservez les validateurs personnalisés sous app/validators
.
Envisagez d'extraire les validateurs personnalisés vers une gemme partagée si vous gérez plusieurs applications associées ou si les validateurs sont suffisamment génériques.
Utilisez librement les étendues nommées.
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
Lorsqu'une portée nommée définie avec un lambda et des paramètres devient trop compliquée, il est préférable de créer à la place une méthode de classe qui sert le même objectif que la portée nommée et renvoie un objet ActiveRecord::Relation
. Vous pouvez sans doute définir des étendues encore plus simples comme celle-ci.
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
Triez les déclarations de rappel dans l’ordre dans lequel elles seront exécutées. Pour référence, voir Rappels disponibles.
# bad
class Person
after_commit :after_commit_callback
before_validation :before_validation_callback
end
# good
class Person
before_validation :before_validation_callback
after_commit :after_commit_callback
end
Méfiez-vous du comportement des méthodes suivantes. Ils n'exécutent pas les validations du modèle et pourraient facilement corrompre l'état du modèle.
# bad
Article . first . decrement! ( :view_count )
DiscussionBoard . decrement_counter ( :post_count , 5 )
Article . first . increment! ( :view_count )
DiscussionBoard . increment_counter ( :post_count , 5 )
person . toggle :active
product . touch
Billing . update_all ( "category = 'authorized', author = 'David'" )
user . update_attribute ( :website , 'example.com' )
user . update_columns ( last_request_at : Time . current )
Post . update_counters 5 , comment_count : - 1 , action_count : 1
# good
user . update_attributes ( website : 'example.com' )
Utilisez des URL conviviales. Afficher un attribut descriptif du modèle dans l'URL plutôt que son id
. Il existe plusieurs façons d’y parvenir.
to_param
du modèle Cette méthode est utilisée par Rails pour construire une URL vers l'objet. L'implémentation par défaut renvoie l' id
de l'enregistrement sous forme de chaîne. Il pourrait être remplacé pour inclure un autre attribut lisible par l'homme.
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
Afin de convertir ceci en une valeur conviviale pour les URL, parameterize
doit être appelé sur la chaîne. L' id
de l'objet doit être au début pour qu'il puisse être trouvé par la méthode find
d'Active Record.
friendly_id
Gemme Il permet la création d'URL lisibles par l'homme en utilisant un attribut descriptif du modèle au lieu de son id
.
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
Consultez la documentation de gem pour plus d'informations sur son utilisation.
find_each
Utilisez find_each
pour parcourir une collection d’objets AR. Parcourir une collection d'enregistrements de la base de données (en utilisant la méthode all
, par exemple) est très inefficace car il tentera d'instancier tous les objets à la fois. Dans ce cas, les méthodes de traitement par lots vous permettent de travailler avec les enregistrements par lots, réduisant ainsi considérablement la consommation de mémoire.
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |