Oleh Bozhidar Batov
Teladan itu penting.
— Petugas Alex J. Murphy / RoboCop
Tip | Anda dapat menemukan versi cantik dari panduan ini dengan navigasi yang jauh lebih baik di https://rails.rubystyle.guide. |
Tujuan dari panduan ini adalah untuk menyajikan serangkaian praktik terbaik dan resep gaya untuk pengembangan Ruby on Rails. Ini adalah panduan pelengkap untuk panduan gaya pengkodean Ruby berbasis komunitas yang sudah ada.
Panduan gaya Rails ini merekomendasikan praktik terbaik sehingga pemrogram Rails di dunia nyata dapat menulis kode yang dapat dikelola oleh pemrogram Rails di dunia nyata lainnya. Panduan gaya yang mencerminkan penggunaan di dunia nyata akan digunakan, dan panduan gaya yang berpegang pada cita-cita yang telah ditolak oleh orang-orang yang seharusnya membantu berisiko tidak digunakan sama sekali - tidak peduli seberapa bagusnya.
Panduan ini dipisahkan menjadi beberapa bagian aturan terkait. Saya telah mencoba menambahkan alasan di balik aturan tersebut (jika dihilangkan, saya berasumsi itu cukup jelas).
Saya tidak membuat semua aturan secara tiba-tiba - aturan tersebut sebagian besar didasarkan pada karir saya yang luas sebagai insinyur perangkat lunak profesional, umpan balik dan saran dari anggota komunitas Rails dan berbagai sumber daya pemrograman Rails yang sangat dihormati.
Catatan | Beberapa saran di sini hanya berlaku untuk Rails versi terbaru. |
Anda dapat membuat salinan PDF dari panduan ini menggunakan AsciiDoctor PDF, dan salinan HTML dengan AsciiDoctor menggunakan perintah berikut:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Tip | Instal permata gem install rouge |
Terjemahan panduan ini tersedia dalam bahasa berikut:
Jepang
Rusia
Tip | RuboCop, penganalisis kode statis (linter) dan pemformat, memiliki ekstensi rubocop-rails , berdasarkan panduan gaya ini. |
Masukkan kode inisialisasi khusus di config/initializers
. Kode dalam penginisialisasi dijalankan saat aplikasi dimulai.
Simpan kode inisialisasi untuk setiap permata dalam file terpisah dengan nama yang sama dengan permata tersebut, misalnya carrierwave.rb
, active_admin.rb
, dll.
Sesuaikan pengaturan untuk lingkungan pengembangan, pengujian dan produksi (dalam file terkait di bawah config/environments/
)
Tandai aset tambahan untuk kompilasi awal (jika ada):
# 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 )
Simpan konfigurasi yang berlaku untuk semua lingkungan di file config/application.rb
.
Saat meningkatkan ke versi Rails yang lebih baru, pengaturan konfigurasi aplikasi Anda akan tetap pada versi sebelumnya. Untuk memanfaatkan praktik Rails terbaru yang direkomendasikan, pengaturan config.load_defaults
harus sesuai dengan versi Rails Anda.
# good
config . load_defaults 6.1
Hindari membuat konfigurasi lingkungan tambahan selain default development
, test
, dan production
. Jika Anda memerlukan lingkungan seperti produksi seperti staging, gunakan variabel lingkungan untuk opsi konfigurasi.
Simpan konfigurasi tambahan apa pun dalam file YAML di bawah direktori config/
.
Karena file konfigurasi Rails 4.2 YAML dapat dengan mudah dimuat dengan metode config_for
baru:
Rails :: Application . config_for ( :yaml_file )
Saat Anda perlu menambahkan lebih banyak tindakan ke sumber daya RESTful (apakah Anda benar-benar membutuhkannya?) gunakan rute member
dan 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
Jika Anda perlu menentukan beberapa rute member/collection
gunakan sintaks blok alternatif.
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
Gunakan rute bertingkat untuk mengekspresikan hubungan antara model Rekaman Aktif dengan lebih baik.
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
Jika Anda perlu membuat rute lebih dari 1 level, gunakan opsi shallow: true
. Ini akan menyelamatkan pengguna dari URL panjang posts/1/comments/5/versions/7/edit
dan Anda dari pembantu URL panjang edit_post_comment_version
.
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
Gunakan rute dengan namespace untuk mengelompokkan tindakan terkait.
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
Jangan pernah menggunakan rute pengontrol liar yang lama. Rute ini akan membuat semua tindakan di setiap pengontrol dapat diakses melalui permintaan GET.
# very bad
match ':controller(/:action(/:id(.:format)))'
Jangan gunakan match
untuk menentukan rute apa pun kecuali ada kebutuhan untuk memetakan beberapa jenis permintaan di antara [:get, :post, :patch, :put, :delete]
ke satu tindakan menggunakan opsi :via
.
Jaga agar pengontrol tetap ramping - pengontrol hanya boleh mengambil data untuk lapisan tampilan dan tidak boleh berisi logika bisnis apa pun (semua logika bisnis secara alami harus berada dalam model).
Setiap tindakan pengontrol (idealnya) harus memanggil hanya satu metode selain penemuan awal atau metode baru.
Minimalkan jumlah variabel instan yang diteruskan antara pengontrol dan tampilan.
Tindakan pengontrol yang ditentukan dalam opsi Filter Tindakan harus berada dalam cakupan leksikal. ActionFilter yang ditentukan untuk tindakan yang diwariskan mempersulit pemahaman cakupan dampaknya terhadap tindakan tersebut.
# 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
Lebih suka menggunakan templat daripada rendering sebaris.
# 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
Lebih suka render plain:
daripada 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!'
...
Lebih memilih simbol yang sesuai dengan kode status HTTP numerik. Angka tersebut bermakna dan tidak terlihat seperti angka "ajaib" untuk kode status HTTP yang kurang dikenal.
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
Perkenalkan kelas model Rekaman Non-Aktif secara bebas.
Beri nama model dengan nama yang bermakna (tetapi pendek) tanpa singkatan.
Jika Anda memerlukan objek yang mendukung perilaku seperti ActiveRecord (seperti validasi) tanpa fungsionalitas database, gunakan 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
Dimulai dengan Rails 6.1, Anda juga dapat memperluas atribut API dari ActiveRecord menggunakan 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
Kecuali jika metode tersebut memiliki arti dalam domain bisnis, jangan masukkan metode ke dalam model Anda yang hanya memformat data Anda (seperti pembuatan kode HTML). Metode-metode ini kemungkinan besar akan dipanggil dari lapisan tampilan saja, jadi tempatnya ada di pembantu. Pertahankan model Anda hanya untuk logika bisnis dan persistensi data.
Hindari mengubah default Rekaman Aktif (nama tabel, kunci utama, dll) kecuali Anda memiliki alasan yang sangat bagus (seperti database yang tidak berada di bawah kendali Anda).
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
Hindari menyetel ignored_columns
. Ini mungkin menimpa tugas sebelumnya dan itu hampir selalu merupakan kesalahan. Lebih baik menambahkan ke daftar saja.
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
Lebih suka menggunakan sintaks hash untuk enum
. Array membuat nilai database tersirat & setiap penyisipan/penghapusan/penataan ulang nilai di tengah kemungkinan besar akan menyebabkan kode rusak.
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
Kelompokkan metode gaya makro ( has_many
, validates
, dll) di awal definisi kelas.
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
Lebih suka has_many :through
has_and_belongs_to_many
. Menggunakan has_many :through
memungkinkan atribut dan validasi tambahan pada model gabungan.
# 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
Lebih suka self[:attribute]
daripada read_attribute(:attribute)
.
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
Lebih suka self[:attribute] = value
daripada write_attribute(:attribute, value)
.
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
Selalu gunakan validasi "gaya baru".
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
Saat memberi nama metode validasi khusus, patuhi aturan sederhana:
validate :method_name
dibaca seperti pernyataan alami
nama metode menjelaskan apa yang diperiksanya
metode ini dikenali sebagai metode validasi berdasarkan namanya, bukan metode predikatnya
# 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
Agar validasi mudah dibaca, jangan mencantumkan beberapa atribut per validasi.
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
Jika validasi khusus digunakan lebih dari sekali atau validasinya berupa pemetaan ekspresi reguler, buat file validator khusus.
# 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
Simpan validator khusus di bawah app/validators
.
Pertimbangkan untuk mengekstrak validator khusus ke permata bersama jika Anda memelihara beberapa aplikasi terkait atau validatornya cukup umum.
Gunakan cakupan bernama dengan bebas.
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
Ketika cakupan bernama yang didefinisikan dengan lambda dan parameter menjadi terlalu rumit, lebih baik membuat metode kelas yang melayani tujuan yang sama dengan cakupan bernama dan mengembalikan objek ActiveRecord::Relation
. Bisa dibilang Anda dapat mendefinisikan cakupan yang lebih sederhana seperti ini.
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
Pesan deklarasi panggilan balik sesuai urutan eksekusinya. Untuk referensi, lihat Panggilan Balik yang Tersedia.
# 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
Waspadai perilaku metode berikut. Mereka tidak menjalankan validasi model dan dapat dengan mudah merusak status model.
# 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' )
Gunakan URL yang mudah digunakan. Tampilkan beberapa atribut deskriptif model di URL, bukan id
-nya. Ada lebih dari satu cara untuk mencapai hal ini.
to_param
Model Metode ini digunakan oleh Rails untuk membuat URL ke objek. Implementasi default mengembalikan id
record sebagai String. Ini dapat diganti untuk menyertakan atribut lain yang dapat dibaca manusia.
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
Untuk mengonversinya menjadi nilai ramah URL, parameterize
harus dipanggil pada string. id
objek harus berada di awal agar dapat ditemukan dengan metode find
pada Active Record.
friendly_id
Permata Ini memungkinkan pembuatan URL yang dapat dibaca manusia dengan menggunakan beberapa atribut deskriptif model, bukan id
-nya.
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
Periksa dokumentasi permata untuk informasi lebih lanjut tentang penggunaannya.
find_each
Gunakan find_each
untuk mengulangi kumpulan objek AR. Perulangan kumpulan record dari database (menggunakan metode all
, misalnya) sangat tidak efisien karena akan mencoba membuat instance semua objek sekaligus. Dalam hal ini, metode pemrosesan batch memungkinkan Anda bekerja dengan catatan dalam batch, sehingga sangat mengurangi konsumsi memori.
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |