Por Bozhidar Batsov
Los modelos a seguir son importantes.
— Oficial Alex J. Murphy / RoboCop
Consejo | Puede encontrar una hermosa versión de esta guía con una navegación muy mejorada en https://rails.rubystyle.guide. |
El objetivo de esta guía es presentar un conjunto de mejores prácticas y prescripciones de estilo para el desarrollo de Ruby on Rails. Es una guía complementaria a la guía de estilo de codificación Ruby ya existente impulsada por la comunidad.
Esta guía de estilo de Rails recomienda las mejores prácticas para que los programadores de Rails del mundo real puedan escribir código que otros programadores de Rails del mundo real puedan mantener. Se utiliza una guía de estilo que refleja el uso en el mundo real, y una guía de estilo que se atiene a un ideal que ha sido rechazado por las personas a las que se supone debe ayudar corre el riesgo de no usarse en absoluto, sin importar lo buena que sea.
La guía está dividida en varias secciones de reglas relacionadas. Intenté agregar la justificación detrás de las reglas (si se omite, asumí que es bastante obvio).
No se me ocurrieron todas las reglas de la nada: se basan principalmente en mi extensa carrera como ingeniero de software profesional, comentarios y sugerencias de miembros de la comunidad Rails y varios recursos de programación Rails de gran prestigio.
Nota | Algunos de los consejos aquí son aplicables sólo a versiones recientes de Rails. |
Puede generar una copia en PDF de esta guía usando AsciiDoctor PDF y una copia HTML con AsciiDoctor usando los siguientes comandos:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Consejo | Instale la gema gem install rouge |
Las traducciones de la guía están disponibles en los siguientes idiomas:
japonés
ruso
Consejo | RuboCop, un analizador de código estático (linter) y formateador, tiene una extensión rubocop-rails , basada en esta guía de estilo. |
Coloque el código de inicialización personalizado en config/initializers
. El código de los inicializadores se ejecuta al iniciar la aplicación.
Mantenga el código de inicialización para cada gema en un archivo separado con el mismo nombre que la gema, por ejemplo, carrierwave.rb
, active_admin.rb
, etc.
Ajuste en consecuencia la configuración para el entorno de desarrollo, prueba y producción (en los archivos correspondientes en config/environments/
)
Marque recursos adicionales para precompilación (si corresponde):
# 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 )
Mantenga la configuración aplicable a todos los entornos en el archivo config/application.rb
.
Al actualizar a una versión más nueva de Rails, la configuración de su aplicación permanecerá en la versión anterior. Para aprovechar las últimas prácticas recomendadas de Rails, la configuración config.load_defaults
debe coincidir con su versión de Rails.
# good
config . load_defaults 6.1
Evite crear configuraciones de entorno adicionales a las predeterminadas de development
, test
y production
. Si necesita un entorno similar a la producción, como una prueba, utilice variables de entorno para las opciones de configuración.
Mantenga cualquier configuración adicional en archivos YAML en el directorio config/
.
Dado que los archivos de configuración YAML de Rails 4.2 se pueden cargar fácilmente con el nuevo método config_for
:
Rails :: Application . config_for ( :yaml_file )
Cuando necesite agregar más acciones a un recurso RESTful (¿realmente las necesita?), utilice rutas member
y 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 necesita definir varias rutas member/collection
utilice la sintaxis de bloque alternativa.
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
Utilice rutas anidadas para expresar mejor la relación entre los modelos de 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 necesita anidar rutas de más de 1 nivel de profundidad, utilice la opción shallow: true
. Esto salvará al usuario de posts/1/comments/5/versions/7/edit
y a usted de los ayudantes de URL largas edit_post_comment_version
.
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
Utilice rutas con espacios de nombres para agrupar acciones relacionadas.
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
Nunca utilices la ruta del controlador salvaje heredado. Esta ruta hará que todas las acciones en cada controlador sean accesibles a través de solicitudes GET.
# very bad
match ':controller(/:action(/:id(.:format)))'
No utilice match
para definir rutas a menos que sea necesario asignar varios tipos de solicitudes entre [:get, :post, :patch, :put, :delete]
a una sola acción usando la opción :via
.
Mantenga los controladores delgados: solo deben recuperar datos para la capa de vista y no deben contener ninguna lógica de negocios (toda la lógica de negocios debe residir naturalmente en el modelo).
Cada acción del controlador debería (idealmente) invocar solo un método distinto de una búsqueda inicial o nueva.
Minimice la cantidad de variables de instancia pasadas entre un controlador y una vista.
Las acciones del controlador especificadas en la opción Filtro de acciones deben tener un alcance léxico. El ActionFilter especificado para una acción heredada dificulta comprender el alcance de su impacto en esa acción.
# 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
Prefiere usar una plantilla en lugar de renderizado en línea.
# 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
Prefiere render plain:
sobre 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!'
...
Prefiera los símbolos correspondientes a los códigos de estado HTTP numéricos. Son significativos y no parecen números "mágicos" de códigos de estado HTTP menos conocidos.
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
Introduzca clases de modelos no Active Record libremente.
Nombra los modelos con nombres significativos (pero cortos) sin abreviaturas.
Si necesita objetos que admitan un comportamiento similar al de ActiveRecord (como validaciones) sin la funcionalidad de la base de datos, utilice 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 de Rails 6.1, también puede ampliar la API de atributos desde 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 tengan algún significado en el ámbito empresarial, no incluya métodos en su modelo que simplemente formatee sus datos (como código que genera HTML). Lo más probable es que estos métodos se llamen únicamente desde la capa de vista, por lo que su lugar está en los ayudantes. Mantenga sus modelos únicamente para la lógica empresarial y la persistencia de datos.
Evite alterar los valores predeterminados de Active Record (nombres de tablas, clave principal, etc.) a menos que tenga una muy buena razón (como una base de datos que no está bajo su control).
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
Evite configurar ignored_columns
. Puede sobrescribir asignaciones anteriores y eso casi siempre es un error. Prefiero agregarlo a la 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
Prefiere usar la sintaxis hash para enum
. La matriz hace que los valores de la base de datos sean implícitos y cualquier inserción/eliminación/reorganización de valores en el medio probablemente conducirá a un código roto.
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.) al comienzo de la definición de clase.
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
Prefiera has_many :through
a has_and_belongs_to_many
. El uso de has_many :through
permite validaciones y atributos adicionales en el modelo de unión.
# 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
Prefiere self[:attribute]
sobre read_attribute(:attribute)
.
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
Prefiere self[:attribute] = value
sobre write_attribute(:attribute, value)
.
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
Utilice siempre las validaciones de "nuevo estilo".
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
Al nombrar métodos de validación personalizados, siga estas reglas simples:
validate :method_name
se lee como una declaración natural
el nombre del método explica lo que comprueba
el método es reconocible como un método de validación por su nombre, no como un 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 que las validaciones sean fáciles de leer, no incluya varios atributos por validación.
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
Cuando una validación personalizada se utiliza más de una vez o la validación es alguna asignación de expresión regular, cree un archivo de validación personalizado.
# 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
Mantenga los validadores personalizados en app/validators
.
Considere extraer validadores personalizados a una gema compartida si mantiene varias aplicaciones relacionadas o si los validadores son lo suficientemente genéricos.
Utilice ámbitos con nombre libremente.
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
Cuando un ámbito con nombre definido con una lambda y parámetros se vuelve demasiado complicado, es preferible crear un método de clase que tenga el mismo propósito que el ámbito con nombre y devuelva un objeto ActiveRecord::Relation
. Podría decirse que puedes definir ámbitos aún más simples como este.
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
Ordene las declaraciones de devolución de llamada en el orden en que se ejecutarán. Como referencia, consulte Devoluciones de llamada 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
Tenga cuidado con el comportamiento de los siguientes métodos. No ejecutan las validaciones del modelo y podrían corromper fácilmente el estado del 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' )
Utilice URL fáciles de usar. Muestra algún atributo descriptivo del modelo en la URL en lugar de su id
. Hay más de una manera de lograrlo.
to_param
del modelo Rails utiliza este método para construir una URL al objeto. La implementación predeterminada devuelve la id
del registro como una cadena. Podría anularse para incluir otro atributo legible por humanos.
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
Para convertir esto en un valor compatible con URL, se debe llamar a parameterize
en la cadena. La id
del objeto debe estar al principio para que pueda encontrarlo mediante el método find
de Active Record.
friendly_id
Permite la creación de URL legibles por humanos mediante el uso de algún atributo descriptivo del modelo en lugar de su id
.
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
Consulte la documentación de la gema para obtener más información sobre su uso.
find_each
Utilice find_each
para iterar sobre una colección de objetos AR. Recorrer una colección de registros de la base de datos (usando el método all
, por ejemplo) es muy ineficiente ya que intentará crear instancias de todos los objetos a la vez. En ese caso, los métodos de procesamiento por lotes le permiten trabajar con los registros en lotes, reduciendo así en gran medida el consumo de memoria.
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |