Von Bozhidar Batsov
Vorbilder sind wichtig.
— Officer Alex J. Murphy / RoboCop
Tipp | Eine schöne Version dieses Leitfadens mit deutlich verbesserter Navigation finden Sie unter https://rails.rubystyle.guide. |
Ziel dieses Leitfadens ist es, eine Reihe von Best Practices und Stilvorgaben für die Ruby on Rails-Entwicklung vorzustellen. Es handelt sich um einen ergänzenden Leitfaden zum bereits vorhandenen Community-gesteuerten Ruby-Coding-Styleguide.
Dieser Rails-Styleguide empfiehlt Best Practices, damit reale Rails-Programmierer Code schreiben können, der von anderen realen Rails-Programmierern gepflegt werden kann. Ein Styleguide, der den realen Gebrauch widerspiegelt, wird verwendet, und ein Styleguide, der an einem Ideal festhält, das von den Menschen, denen er helfen soll, abgelehnt wurde, riskiert, dass er überhaupt nicht verwendet wird – egal wie gut er ist.
Der Leitfaden ist in mehrere Abschnitte mit zugehörigen Regeln unterteilt. Ich habe versucht, die Begründung hinter den Regeln hinzuzufügen (wenn sie weggelassen wird, gehe ich davon aus, dass sie ziemlich offensichtlich ist).
Ich habe mir nicht alle Regeln aus dem Nichts ausgedacht – sie basieren größtenteils auf meiner langen Karriere als professioneller Softwareentwickler, Feedback und Vorschlägen von Mitgliedern der Rails-Community und verschiedenen hoch angesehenen Rails-Programmierressourcen.
Notiz | Einige der Ratschläge hier gelten nur für neuere Versionen von Rails. |
Mit den folgenden Befehlen können Sie mit AsciiDoctor PDF eine PDF-Kopie dieses Handbuchs und mit AsciiDoctor eine HTML-Kopie erstellen:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Tipp | Installieren Sie den gem install rouge |
Übersetzungen des Leitfadens sind in den folgenden Sprachen verfügbar:
japanisch
Russisch
Tipp | RuboCop, ein statischer Code-Analysator (Linter) und Formatierer, verfügt über eine rubocop-rails Erweiterung, die auf diesem Styleguide basiert. |
Fügen Sie benutzerdefinierten Initialisierungscode in config/initializers
. Der Code in Initialisierern wird beim Anwendungsstart ausgeführt.
Bewahren Sie den Initialisierungscode für jedes Gem in einer separaten Datei mit demselben Namen wie das Gem auf, z. B. carrierwave.rb
, active_admin.rb
“ usw.
Passen Sie die Einstellungen für Entwicklungs-, Test- und Produktionsumgebung entsprechend an (in den entsprechenden Dateien unter config/environments/
)
Markieren Sie zusätzliche Assets für die Vorkompilierung (falls vorhanden):
# 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 )
Behalten Sie die für alle Umgebungen gültige Konfiguration in der Datei config/application.rb
bei.
Beim Upgrade auf eine neuere Rails-Version bleiben die Konfigurationseinstellungen Ihrer Anwendung auf der vorherigen Version. Um die neuesten empfohlenen Rails-Praktiken nutzen zu können, sollte die Einstellung config.load_defaults
mit Ihrer Rails-Version übereinstimmen.
# good
config . load_defaults 6.1
Vermeiden Sie die Erstellung zusätzlicher Umgebungskonfigurationen als die Standardeinstellungen für development
, test
und production
. Wenn Sie eine produktionsähnliche Umgebung wie Staging benötigen, verwenden Sie Umgebungsvariablen für Konfigurationsoptionen.
Bewahren Sie alle zusätzlichen Konfigurationen in YAML-Dateien im Verzeichnis config/
auf.
Seit Rails 4.2 können YAML-Konfigurationsdateien einfach mit der neuen config_for
-Methode geladen werden:
Rails :: Application . config_for ( :yaml_file )
Wenn Sie einer RESTful-Ressource weitere Aktionen hinzufügen müssen (brauchen Sie diese überhaupt?), verwenden Sie member
und collection
Routen.
# 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
Wenn Sie mehrere member/collection
Sammlungsrouten definieren müssen, verwenden Sie die alternative Blocksyntax.
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
Verwenden Sie verschachtelte Routen, um die Beziehung zwischen Active Record-Modellen besser auszudrücken.
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
Wenn Sie Routen mehr als eine Ebene tief verschachteln müssen, verwenden Sie die Option shallow: true
. Dies erspart dem Benutzer die langen URLs posts/1/comments/5/versions/7/edit
und Ihnen die langen URL-Helfer edit_post_comment_version
.
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
Verwenden Sie Namensraumrouten, um verwandte Aktionen zu gruppieren.
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
Verwenden Sie niemals die alte Wild-Controller-Route. Diese Route macht alle Aktionen in jedem Controller über GET-Anfragen zugänglich.
# very bad
match ':controller(/:action(/:id(.:format)))'
Verwenden Sie match
nicht, um Routen zu definieren, es sei denn, es besteht die Notwendigkeit, mehrere Anforderungstypen unter [:get, :post, :patch, :put, :delete]
mithilfe der Option :via
einer einzelnen Aktion zuzuordnen.
Halten Sie die Controller schlank – sie sollten nur Daten für die Ansichtsebene abrufen und keine Geschäftslogik enthalten (die gesamte Geschäftslogik sollte sich natürlich im Modell befinden).
Jede Controller-Aktion sollte (idealerweise) nur eine andere Methode als „initial find“ oder „new“ aufrufen.
Minimieren Sie die Anzahl der Instanzvariablen, die zwischen einem Controller und einer Ansicht übergeben werden.
In der Option „Aktionsfilter“ angegebene Controller-Aktionen sollten sich im lexikalischen Bereich befinden. Der für eine geerbte Aktion angegebene ActionFilter macht es schwierig, den Umfang seiner Auswirkungen auf diese Aktion zu verstehen.
# 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
Verwenden Sie lieber eine Vorlage als Inline-Rendering.
# 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
Bevorzugen Sie render plain:
gegenüber 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!'
...
Bevorzugen Sie entsprechende Symbole gegenüber numerischen HTTP-Statuscodes. Sie sind aussagekräftig und sehen für weniger bekannte HTTP-Statuscodes nicht wie „magische“ Zahlen aus.
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
Führen Sie nach Belieben Nicht-Active-Record-Modellklassen ein.
Benennen Sie die Modelle mit aussagekräftigen (aber kurzen) Namen ohne Abkürzungen.
Wenn Sie Objekte benötigen, die ActiveRecord-ähnliches Verhalten (wie Validierungen) ohne Datenbankfunktionalität unterstützen, verwenden Sie 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
Ab Rails 6.1 können Sie die Attribut-API auch von ActiveRecord mithilfe von ActiveModel::Attributes
erweitern.
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
Sofern sie im geschäftlichen Bereich keine Bedeutung haben, fügen Sie in Ihr Modell keine Methoden ein, die lediglich Ihre Daten formatieren (z. B. Code, der HTML generiert). Diese Methoden werden höchstwahrscheinlich nur von der Ansichtsebene aus aufgerufen, sodass ihr Platz in den Hilfsprogrammen liegt. Behalten Sie Ihre Modelle nur für Geschäftslogik und Datenpersistenz.
Vermeiden Sie es, die Standardeinstellungen für aktive Datensätze (Tabellennamen, Primärschlüssel usw.) zu ändern, es sei denn, Sie haben einen sehr guten Grund (z. B. eine Datenbank, die nicht unter Ihrer Kontrolle steht).
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
anhängen Vermeiden Sie das Festlegen von ignored_columns
. Es kann sein, dass frühere Aufgaben überschrieben werden, und das ist fast immer ein Fehler. Stattdessen lieber an die Liste anhängen.
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
Verwenden Sie für enum
lieber die Hash-Syntax. Das Array macht die Datenbankwerte implizit und jedes Einfügen/Entfernen/Neuanordnen von Werten in der Mitte führt höchstwahrscheinlich zu fehlerhaftem Code.
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
Gruppieren Sie Methoden im Makrostil ( has_many
, validates
usw.) am Anfang der Klassendefinition.
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
Bevorzugen Sie has_many :through
has_and_belongs_to_many
. Die Verwendung von has_many :through
ermöglicht zusätzliche Attribute und Validierungen für das Join-Modell.
# 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
Bevorzugen Sie self[:attribute]
gegenüber read_attribute(:attribute)
.
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
Bevorzugen Sie self[:attribute] = value
gegenüber write_attribute(:attribute, value)
.
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
Verwenden Sie immer die Validierungen „neuen Stils“.
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
Halten Sie sich bei der Benennung benutzerdefinierter Validierungsmethoden an die einfachen Regeln:
validate :method_name
liest sich wie eine natürliche Aussage
Der Methodenname erklärt, was überprüft wird
Die Methode ist anhand ihres Namens als Validierungsmethode und nicht als Prädikatmethode erkennbar
# 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
Damit Validierungen leichter lesbar sind, sollten Sie nicht mehrere Attribute pro Validierung auflisten.
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
Wenn eine benutzerdefinierte Validierung mehr als einmal verwendet wird oder es sich bei der Validierung um eine Zuordnung regulärer Ausdrücke handelt, erstellen Sie eine benutzerdefinierte Validatordatei.
# 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
Behalten Sie benutzerdefinierte Validatoren unter app/validators
.
Erwägen Sie das Extrahieren benutzerdefinierter Validatoren in ein gemeinsames Gem, wenn Sie mehrere verwandte Apps verwalten oder die Validatoren generisch genug sind.
Benannte Bereiche können frei verwendet werden.
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
Wenn ein mit einem Lambda und Parametern definierter benannter Bereich zu kompliziert wird, ist es vorzuziehen, stattdessen eine Klassenmethode zu erstellen, die denselben Zweck wie der benannte Bereich erfüllt und ein ActiveRecord::Relation
Objekt zurückgibt. Möglicherweise können Sie noch einfachere Bereiche wie diesen definieren.
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
Ordnen Sie Callback-Deklarationen in der Reihenfolge an, in der sie ausgeführt werden. Als Referenz siehe Verfügbare Rückrufe.
# 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
Beachten Sie das Verhalten der folgenden Methoden. Sie führen keine Modellvalidierungen durch und könnten leicht den Modellstatus beschädigen.
# 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' )
Verwenden Sie benutzerfreundliche URLs. Zeigen Sie ein beschreibendes Attribut des Modells in der URL anstelle seiner id
an. Es gibt mehr als einen Weg, dies zu erreichen.
to_param
-Methode des Modells Diese Methode wird von Rails zum Erstellen einer URL zum Objekt verwendet. Die Standardimplementierung gibt die id
des Datensatzes als String zurück. Es könnte überschrieben werden, um ein anderes für Menschen lesbares Attribut aufzunehmen.
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
Um dies in einen URL-freundlichen Wert umzuwandeln, sollte parameterize
für die Zeichenfolge aufgerufen werden. Die id
des Objekts muss am Anfang stehen, damit es von der find
von Active Record gefunden werden kann.
friendly_id
Gem Es ermöglicht die Erstellung von für Menschen lesbaren URLs, indem ein beschreibendes Attribut des Modells anstelle seiner id
verwendet wird.
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
Weitere Informationen zur Verwendung finden Sie in der Gem-Dokumentation.
find_each
Verwenden Sie find_each
, um eine Sammlung von AR-Objekten zu durchlaufen. Das Durchlaufen einer Sammlung von Datensätzen aus der Datenbank (z. B. mit der all
-Methode) ist sehr ineffizient, da versucht wird, alle Objekte auf einmal zu instanziieren. In diesem Fall können Sie mit Stapelverarbeitungsmethoden stapelweise mit den Datensätzen arbeiten und so den Speicherverbrauch erheblich reduzieren.
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |