โดย โบซิดาร์ บัตซอฟ
แบบอย่างเป็นสิ่งสำคัญ
— เจ้าหน้าที่อเล็กซ์ เจ. เมอร์ฟี่ / RoboCop
เคล็ดลับ | คุณจะพบคู่มือฉบับสวยงามพร้อมการนำทางที่ได้รับการปรับปรุงให้ดีขึ้นมากได้ที่ https://rails.rubystyle.guide |
เป้าหมายของคู่มือนี้คือการนำเสนอชุดแนวทางปฏิบัติที่ดีที่สุดและข้อกำหนดด้านสไตล์สำหรับการพัฒนา Ruby on Rails เป็นคู่มือเสริมสำหรับคู่มือสไตล์การเขียนโค้ด Ruby ที่ขับเคลื่อนโดยชุมชนที่มีอยู่แล้ว
คู่มือสไตล์ Rails นี้แนะนำแนวทางปฏิบัติที่ดีที่สุดเพื่อให้โปรแกรมเมอร์ Rails ในโลกแห่งความเป็นจริงสามารถเขียนโค้ดที่โปรแกรมเมอร์ Rails ในโลกแห่งความเป็นจริงคนอื่นๆ สามารถดูแลรักษาได้ คู่มือสไตล์ที่สะท้อนถึงการใช้งานในโลกแห่งความเป็นจริง และคู่มือสไตล์ที่ยึดถืออุดมคติที่ถูกปฏิเสธโดยผู้คน ซึ่งควรจะช่วยเสี่ยงต่อการไม่ถูกใช้งานเลย ไม่ว่ามันจะดีแค่ไหนก็ตาม
คู่มือนี้แบ่งออกเป็นกฎที่เกี่ยวข้องหลายส่วน ฉันได้พยายามเพิ่มเหตุผลเบื้องหลังกฎต่างๆ (หากละเว้น ฉันคิดว่ามันค่อนข้างชัดเจน)
ฉันไม่ได้คิดกฎเกณฑ์ขึ้นมาเอง ส่วนใหญ่จะขึ้นอยู่กับอาชีพการงานของฉันในฐานะวิศวกรซอฟต์แวร์มืออาชีพ ความคิดเห็นและข้อเสนอแนะจากสมาชิกของชุมชน Rails และแหล่งข้อมูลการเขียนโปรแกรม Rails ที่ได้รับการยกย่องอย่างสูงต่างๆ
บันทึก | คำแนะนำบางส่วนที่นี่ใช้ได้กับ Rails เวอร์ชันล่าสุดเท่านั้น |
คุณสามารถสร้างสำเนา PDF ของคู่มือนี้โดยใช้ AsciiDoctor PDF และสำเนา HTML ด้วย AsciiDoctor โดยใช้คำสั่งต่อไปนี้:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
เคล็ดลับ | ติดตั้งอัญมณี gem install rouge |
คำแปลของคู่มือมีให้บริการในภาษาต่อไปนี้:
ญี่ปุ่น
ภาษารัสเซีย
เคล็ดลับ | RuboCop ซึ่งเป็นเครื่องวิเคราะห์โค้ดแบบคงที่ (linter) และตัวจัดรูปแบบ มีส่วนขยาย rubocop-rails ตามคำแนะนำสไตล์นี้ |
ใส่รหัสเริ่มต้นที่กำหนดเองใน config/initializers
รหัสในเครื่องมือเริ่มต้นจะดำเนินการเมื่อเริ่มต้นแอปพลิเคชัน
เก็บโค้ดเริ่มต้นสำหรับอัญมณีแต่ละรายการไว้ในไฟล์แยกกันโดยใช้ชื่อเดียวกันกับอัญมณี เช่น carrierwave.rb
, active_admin.rb
เป็นต้น
ปรับการตั้งค่าสำหรับสภาพแวดล้อมการพัฒนา การทดสอบ และการใช้งานจริงตามนั้น (ในไฟล์ที่เกี่ยวข้องภายใต้ config/environments/
)
ทำเครื่องหมายเนื้อหาเพิ่มเติมสำหรับการคอมไพล์ล่วงหน้า (ถ้ามี):
# 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 )
เก็บการกำหนดค่าที่ใช้ได้กับทุกสภาพแวดล้อมไว้ในไฟล์ config/application.rb
เมื่ออัปเกรดเป็น Rails เวอร์ชันใหม่ การตั้งค่าการกำหนดค่าแอปพลิเคชันของคุณจะยังคงอยู่ในเวอร์ชันก่อนหน้า เพื่อใช้ประโยชน์จากแนวทางปฏิบัติล่าสุดของ Rails ที่แนะนำ การตั้งค่า config.load_defaults
ควรตรงกับเวอร์ชัน Rails ของคุณ
# good
config . load_defaults 6.1
หลีกเลี่ยงการสร้างการกำหนดค่าสภาพแวดล้อมเพิ่มเติมนอกเหนือจากค่าเริ่มต้นของ development
test
และ production
จริง หากคุณต้องการสภาพแวดล้อมที่เหมือนการใช้งานจริง เช่น การจัดเตรียม ให้ใช้ตัวแปรสภาพแวดล้อมสำหรับตัวเลือกการกำหนดค่า
เก็บการกำหนดค่าเพิ่มเติมไว้ในไฟล์ YAML ใต้ไดเร็กทอรี config/
เนื่องจากสามารถโหลดไฟล์การกำหนดค่า Rails 4.2 YAML ได้อย่างง่ายดายด้วยวิธี config_for
ใหม่:
Rails :: Application . config_for ( :yaml_file )
เมื่อคุณต้องการเพิ่มการดำเนินการเพิ่มเติมให้กับทรัพยากร RESTful (คุณต้องการมันจริงๆ หรือไม่) ให้ใช้เส้นทาง member
และ 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
หากคุณต้องการกำหนดเส้นทาง member/collection
หลายรายการ ให้ใช้ไวยากรณ์บล็อกทางเลือก
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
ใช้เส้นทางที่ซ้อนกันเพื่อแสดงความสัมพันธ์ระหว่างโมเดล Active Record ได้ดีขึ้น
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
หากคุณต้องการซ้อนเส้นทางมากกว่า 1 ระดับ ให้ใช้ตัวเลือก shallow: true
สิ่งนี้จะช่วยให้ผู้ใช้ประหยัดจาก URL แบบยาว posts/1/comments/5/versions/7/edit
และคุณจากตัวช่วย URL แบบยาว edit_post_comment_version
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
ใช้เส้นทางเนมสเปซเพื่อจัดกลุ่มการดำเนินการที่เกี่ยวข้อง
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
ห้ามใช้เส้นทาง Wild Controller แบบเดิม เส้นทางนี้จะทำให้การดำเนินการทั้งหมดในคอนโทรลเลอร์ทุกตัวสามารถเข้าถึงได้ผ่านคำขอ GET
# very bad
match ':controller(/:action(/:id(.:format)))'
อย่าใช้ match
เพื่อกำหนดเส้นทางใดๆ ยกเว้นในกรณีที่จำเป็นต้องแมปคำขอหลายประเภทระหว่าง [:get, :post, :patch, :put, :delete]
กับการดำเนินการเดียวโดยใช้ตัวเลือก :via
รักษาตัวควบคุมให้มีขนาดเล็ก - ควรดึงข้อมูลสำหรับเลเยอร์มุมมองเท่านั้นและไม่ควรมีตรรกะทางธุรกิจใดๆ (ตรรกะทางธุรกิจทั้งหมดควรอยู่ในโมเดลโดยธรรมชาติ)
การกระทำของคอนโทรลเลอร์แต่ละตัวควร (ตามหลักการ) เรียกใช้เพียงวิธีเดียวเท่านั้น นอกเหนือจากการค้นหาครั้งแรกหรือการค้นหาใหม่
ลดจำนวนตัวแปรอินสแตนซ์ที่ส่งผ่านระหว่างตัวควบคุมและมุมมองให้เหลือน้อยที่สุด
การดำเนินการของตัวควบคุมที่ระบุในตัวเลือกของตัวกรองการดำเนินการควรอยู่ในขอบเขตคำศัพท์ ActionFilter ที่ระบุสำหรับการดำเนินการที่สืบทอดมาทำให้เป็นการยากที่จะเข้าใจขอบเขตของผลกระทบต่อการดำเนินการนั้น
# 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
ต้องการใช้เทมเพลตมากกว่าการแสดงผลแบบอินไลน์
# 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
ต้องการ render plain:
มากกว่า 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!'
...
ต้องการสัญลักษณ์ที่สอดคล้องกับรหัสสถานะ HTTP ที่เป็นตัวเลข พวกมันมีความหมายและดูไม่เหมือนตัวเลข "มหัศจรรย์" สำหรับรหัสสถานะ HTTP ที่ไม่ค่อยมีใครรู้จัก
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
แนะนำคลาสแบบจำลอง non-Active Record ได้อย่างอิสระ
ตั้งชื่อโมเดลด้วยชื่อที่มีความหมาย (แต่สั้น) โดยไม่มีตัวย่อ
หากคุณต้องการออบเจ็กต์ที่รองรับพฤติกรรมที่คล้ายกับ ActiveRecord (เช่น การตรวจสอบความถูกต้อง) โดยไม่มีฟังก์ชันฐานข้อมูล ให้ใช้ 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
เริ่มต้นด้วย Rails 6.1 คุณยังสามารถขยาย API แอตทริบิวต์จาก ActiveRecord โดยใช้ 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
เว้นแต่จะมีความหมายบางประการในโดเมนธุรกิจ อย่าใส่วิธีการในโมเดลของคุณที่เพิ่งจัดรูปแบบข้อมูลของคุณ (เช่น การสร้างโค้ด HTML) วิธีการเหล่านี้มักจะถูกเรียกจากเลเยอร์มุมมองเท่านั้น ดังนั้นตำแหน่งของพวกเขาจึงอยู่ในตัวช่วย เก็บโมเดลของคุณไว้สำหรับตรรกะทางธุรกิจและการคงอยู่ของข้อมูลเท่านั้น
หลีกเลี่ยงการเปลี่ยนค่าเริ่มต้นของ Active Record (ชื่อตาราง คีย์หลัก ฯลฯ) เว้นแต่คุณจะมีเหตุผลที่ดี (เช่น ฐานข้อมูลที่ไม่อยู่ภายใต้การควบคุมของคุณ)
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
เสมอ หลีกเลี่ยงการตั้งค่า ignored_columns
มันอาจเขียนทับงานมอบหมายก่อนหน้านี้และนั่นเกือบจะเป็นความผิดพลาดเสมอไป ต้องการต่อท้ายรายการแทน
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
ต้องการใช้ไวยากรณ์แฮชสำหรับ enum
Array ทำให้ค่าฐานข้อมูลโดยนัย & การแทรก/การลบ/การจัดเรียงค่าตรงกลางอาจทำให้โค้ดเสียหายได้
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
วิธีการจัดกลุ่มรูปแบบมาโคร ( has_many
, validates
, ฯลฯ ) ในตอนต้นของการกำหนดคลาส
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
ชอบ has_many :through
ถึง has_and_belongs_to_many
การใช้ has_many :through
อนุญาตให้มีแอตทริบิวต์เพิ่มเติมและการตรวจสอบความถูกต้องของโมเดลการรวม
# 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
ชอบ self[:attribute]
มากกว่า read_attribute(:attribute)
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
ชอบ self[:attribute] = value
มากกว่า write_attribute(:attribute, value)
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
ใช้การตรวจสอบ "รูปแบบใหม่" เสมอ
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
เมื่อตั้งชื่อวิธีการตรวจสอบแบบกำหนดเอง ให้ปฏิบัติตามกฎง่ายๆ:
validate :method_name
อ่านเหมือนคำสั่งที่เป็นธรรมชาติ
ชื่อวิธีการอธิบายสิ่งที่จะตรวจสอบ
วิธีการนี้สามารถรับรู้ได้ว่าเป็นวิธีการตรวจสอบตามชื่อ ไม่ใช่วิธีการเพรดิเคต
# 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
เพื่อให้การตรวจสอบง่ายต่อการอ่าน อย่าระบุแอตทริบิวต์หลายรายการต่อการตรวจสอบ
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
เมื่อใช้การตรวจสอบความถูกต้องแบบกำหนดเองมากกว่าหนึ่งครั้ง หรือการตรวจสอบความถูกต้องคือการแมปนิพจน์ทั่วไป ให้สร้างไฟล์เครื่องมือตรวจสอบแบบกำหนดเอง
# 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
เก็บเครื่องมือตรวจสอบที่กำหนดเองไว้ภายใต้ app/validators
พิจารณาการแยกตัวตรวจสอบแบบกำหนดเองไปยัง Gem ที่แชร์ หากคุณดูแลแอปที่เกี่ยวข้องหลายตัวหรือตัวตรวจสอบนั้นเป็นแบบทั่วไปเพียงพอ
ใช้ขอบเขตที่มีชื่อได้อย่างอิสระ
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
เมื่อขอบเขตที่มีชื่อซึ่งกำหนดด้วยแลมบ์ดาและพารามิเตอร์ซับซ้อนเกินไป ขอแนะนำให้สร้างเมธอดคลาสแทนซึ่งให้บริการตามวัตถุประสงค์เดียวกันของขอบเขตที่มีชื่อและส่งกลับวัตถุ ActiveRecord::Relation
คุณอาจกำหนดขอบเขตที่ง่ายกว่านี้ได้
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
สั่งซื้อการประกาศโทรกลับตามลำดับที่จะดำเนินการ สำหรับการอ้างอิง โปรดดูที่การโทรกลับที่มีอยู่
# 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
ระวังพฤติกรรมของวิธีการดังต่อไปนี้ พวกเขาไม่ได้เรียกใช้การตรวจสอบโมเดลและอาจทำให้สถานะของโมเดลเสียหายได้ง่าย
# 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' )
ใช้ URL ที่ใช้งานง่าย แสดงแอตทริบิวต์เชิงอธิบายของโมเดลใน URL แทนที่จะเป็น id
มีมากกว่าหนึ่งวิธีในการบรรลุเป้าหมายนี้
to_param
ของโมเดล Rails ใช้วิธีนี้ในการสร้าง URL ไปยังวัตถุ การใช้งานเริ่มต้นจะส่งกลับ id
ของเรกคอร์ดเป็นสตริง อาจถูกแทนที่เพื่อรวมคุณลักษณะอื่นที่มนุษย์สามารถอ่านได้
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
ในการแปลงค่านี้เป็นค่าที่เป็นมิตรกับ URL ควรเรียกการกำหนด parameterize
บนสตริง id
ของวัตถุต้องอยู่ที่จุดเริ่มต้นเพื่อให้สามารถค้นหาได้โดยวิธี find
ของ Active Record
friendly_id
อนุญาตให้สร้าง URL ที่มนุษย์สามารถอ่านได้โดยใช้แอตทริบิวต์เชิงอธิบายของโมเดลแทน id
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
ตรวจสอบเอกสารประกอบอัญมณีเพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้งาน
find_each
ใช้ find_each
เพื่อวนซ้ำคอลเลกชันของวัตถุ AR การวนซ้ำคอลเลกชันของบันทึกจากฐานข้อมูล (โดยใช้วิธีการ all
เป็นต้น) นั้นไม่มีประสิทธิภาพมากนัก เนื่องจากจะพยายามสร้างอินสแตนซ์ของวัตถุทั้งหมดในคราวเดียว ในกรณีดังกล่าว วิธีการประมวลผลแบบแบตช์ช่วยให้คุณสามารถทำงานกับบันทึกเป็นแบตช์ได้ ซึ่งช่วยลดการใช้หน่วยความจำได้อย่างมาก
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |