Por Bozhidar Batsov
Os modelos de comportamento são importantes.
- Oficial Alex J. Murphy / RoboCop
Dica | Você pode encontrar uma bela versão deste guia com navegação muito melhorada em https://rails.rubystyle.guide. |
O objetivo deste guia é apresentar um conjunto de melhores práticas e prescrições de estilo para desenvolvimento Ruby on Rails. É um guia complementar ao guia de estilo de codificação Ruby já existente, orientado pela comunidade.
Este guia de estilo Rails recomenda melhores práticas para que programadores Rails do mundo real possam escrever código que possa ser mantido por outros programadores Rails do mundo real. Um guia de estilo que reflete o uso no mundo real é usado, e um guia de estilo que segue um ideal que foi rejeitado pelas pessoas que deveria ajudar corre o risco de não ser usado - não importa quão bom seja.
O guia está separado em diversas seções de regras relacionadas. Tentei adicionar a lógica por trás das regras (se for omitida, presumi que fosse bastante óbvio).
Eu não criei todas as regras do nada - elas são baseadas principalmente em minha extensa carreira como engenheiro de software profissional, feedback e sugestões de membros da comunidade Rails e vários recursos de programação Rails altamente conceituados.
Observação | Alguns dos conselhos aqui são aplicáveis apenas a versões recentes do Rails. |
Você pode gerar uma cópia em PDF deste guia usando AsciiDoctor PDF e uma cópia em HTML com AsciiDoctor usando os seguintes comandos:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Dica | Instale a gema gem install rouge |
As traduções do guia estão disponíveis nos seguintes idiomas:
japonês
russo
Dica | RuboCop, um analisador de código estático (linter) e formatador, possui uma extensão rubocop-rails , baseada neste guia de estilo. |
Coloque o código de inicialização personalizado em config/initializers
. O código nos inicializadores é executado na inicialização do aplicativo.
Mantenha o código de inicialização de cada gema em um arquivo separado com o mesmo nome da gema, por exemplo, carrierwave.rb
, active_admin.rb
, etc.
Ajuste adequadamente as configurações do ambiente de desenvolvimento, teste e produção (nos arquivos correspondentes em config/environments/
)
Marque ativos adicionais para pré-compilação (se houver):
# 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 )
Mantenha a configuração aplicável a todos os ambientes no arquivo config/application.rb
.
Ao atualizar para uma versão mais recente do Rails, a configuração da sua aplicação permanecerá na versão anterior. Para aproveitar as últimas práticas recomendadas do Rails, a configuração config.load_defaults
deve corresponder à sua versão do Rails.
# good
config . load_defaults 6.1
Evite criar configurações de ambiente adicionais além dos padrões de development
, test
e production
. Se você precisar de um ambiente semelhante ao de produção, como teste, use variáveis de ambiente para opções de configuração.
Mantenha qualquer configuração adicional em arquivos YAML no diretório config/
.
Desde o Rails 4.2, os arquivos de configuração YAML podem ser facilmente carregados com o novo método config_for
:
Rails :: Application . config_for ( :yaml_file )
Quando você precisar adicionar mais ações a um recurso RESTful (você realmente precisa delas?) use rotas member
e 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
Se você precisar definir múltiplas rotas member/collection
use a sintaxe de bloco alternativa.
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
Use rotas aninhadas para expressar melhor o relacionamento entre os modelos do Active Record.
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
Se você precisar aninhar rotas com mais de 1 nível de profundidade, use a opção shallow: true
. Isso salvará o usuário de URLs longos posts/1/comments/5/versions/7/edit
e você de ajudantes de URL longos edit_post_comment_version
.
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
Use rotas com namespace para agrupar ações relacionadas.
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
Nunca use a rota do controlador selvagem legado. Esta rota tornará todas as ações em cada controlador acessíveis através de solicitações GET.
# very bad
match ':controller(/:action(/:id(.:format)))'
Não use match
para definir nenhuma rota, a menos que seja necessário mapear vários tipos de solicitação entre [:get, :post, :patch, :put, :delete]
para uma única ação usando a opção :via
.
Mantenha os controladores reduzidos - eles devem recuperar dados apenas para a camada de visualização e não devem conter nenhuma lógica de negócios (toda a lógica de negócios deve residir naturalmente no modelo).
Cada ação do controlador deve (idealmente) invocar apenas um método diferente de um achado inicial ou novo.
Minimize o número de variáveis de instância passadas entre um controlador e uma visualização.
As ações do controlador especificadas na opção Action Filter devem estar no escopo léxico. O ActionFilter especificado para uma ação herdada dificulta a compreensão do escopo do seu impacto nessa ação.
# 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
Prefira usar um modelo em vez de renderização embutida.
# 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
Prefira render plain:
em vez de 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!'
...
Prefira símbolos correspondentes a códigos de status HTTP numéricos. Eles são significativos e não parecem números “mágicos” para códigos de status HTTP menos conhecidos.
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
Introduzir classes de modelo não Active Record livremente.
Nomeie os modelos com nomes significativos (mas curtos) sem abreviações.
Se você precisar de objetos que suportem comportamento semelhante ao ActiveRecord (como validações) sem a funcionalidade de banco de dados, use 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
A partir do Rails 6.1, você também pode estender a API de atributos do ActiveRecord usando 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
A menos que eles tenham algum significado no domínio comercial, não coloque métodos em seu modelo que apenas formatem seus dados (como código que gera HTML). Esses métodos provavelmente serão chamados apenas a partir da camada de visualização, portanto, seu lugar é nos auxiliares. Mantenha seus modelos apenas para lógica de negócios e persistência de dados.
Evite alterar os padrões do Active Record (nomes de tabelas, chave primária, etc.), a menos que você tenha um bom motivo (como um banco de dados que não está sob seu controle).
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
Evite definir ignored_columns
. Pode substituir tarefas anteriores e isso quase sempre é um erro. Prefira anexar à lista.
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
Prefira usar a sintaxe hash para enum
. Array torna os valores do banco de dados implícitos e qualquer inserção/remoção/reorganização de valores no meio provavelmente levará a código quebrado.
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
Agrupe métodos de estilo macro ( has_many
, validates
, etc) no início da definição da 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
Prefira has_many :through
a has_and_belongs_to_many
. Usar has_many :through
permite atributos e validações adicionais no modelo de junção.
# 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
Prefira self[:attribute]
em vez de read_attribute(:attribute)
.
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
Prefira self[:attribute] = value
em vez de write_attribute(:attribute, value)
.
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
Sempre use as validações do "novo estilo".
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
Ao nomear métodos de validação personalizados, siga estas regras simples:
validate :method_name
parece uma declaração natural
o nome do método explica o que ele verifica
o método é reconhecível como um método de validação pelo seu nome, não como um método predicado
# 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
Para facilitar a leitura das validações, não liste vários atributos por validação.
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
Quando uma validação customizada for usada mais de uma vez ou a validação for algum mapeamento de expressão regular, crie um arquivo validador customizado.
# 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
Mantenha validadores personalizados em app/validators
.
Considere extrair validadores personalizados para um gem compartilhado se você estiver mantendo vários aplicativos relacionados ou se os validadores forem genéricos o suficiente.
Use escopos nomeados livremente.
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
Quando um escopo nomeado definido com um lambda e parâmetros se torna muito complicado, é preferível criar um método de classe que atenda ao mesmo propósito do escopo nomeado e retorne um objeto ActiveRecord::Relation
. Provavelmente você pode definir escopos ainda mais simples como este.
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
Ordene as declarações de retorno de chamada na ordem em que serão executadas. Para referência, consulte Retornos de chamada disponíveis.
# 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
Cuidado com o comportamento dos métodos a seguir. Eles não executam as validações do modelo e podem facilmente corromper o estado do modelo.
# 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' )
Use URLs fáceis de usar. Mostre algum atributo descritivo do modelo na URL em vez de seu id
. Há mais de uma maneira de conseguir isso.
to_param
do modelo Este método é usado pelo Rails para construir uma URL para o objeto. A implementação padrão retorna o id
do registro como uma String. Poderia ser substituído para incluir outro atributo legível por humanos.
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
Para converter isso em um valor compatível com URL, parameterize
deve ser chamado na string. O id
do objeto precisa estar no início para que possa ser encontrado pelo método find
do Active Record.
friendly_id
Ele permite a criação de URLs legíveis por humanos usando algum atributo descritivo do modelo em vez de seu id
.
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
Verifique a documentação do gem para obter mais informações sobre seu uso.
find_each
Use find_each
para iterar em uma coleção de objetos AR. Percorrer uma coleção de registros do banco de dados (usando o método all
, por exemplo) é muito ineficiente, pois tentará instanciar todos os objetos de uma vez. Nesse caso, os métodos de processamento em lote permitem trabalhar com os registros em lotes, reduzindo bastante o consumo de memória.
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |