Một Số Method Và Phong Cách Code Hay Trong Rails
Bài đăng này đã không được cập nhật trong 7 năm
I. Một Số Method Hay
1. .nil?
Theo Ruby doc thì: .nil? là một hàm của Object, nên tất cả các object kế thừa từ Object mặc định đều có hàm nil? chỉ có nil object trả về true khi gọi nil?
nil.nil?
# => true
"".nil?
# => false
4.nil?
# => false
Object.new.nil?
# => false
BasicObject.new.nil?
# NoMethodError: undefined method `nil?' for #<BasicObject:0x0000000b794b30> from (pry):72:in `<main>'
2. .empty?
.empty? là function có sẵn của String, Array, Hash
Hiểu một cách đơn giản thì .empty? sẽ trả về true nếu:
string.length == 0 array.length == 0 hash.length == 0 Như vậy,
"".empty?
# => true
[].empty?
# => true
{}.empty?
# => true
" ".empty?
# => false
[nil].empty?
# => false
3. .blank?
.blank? là một hàm rất thú vị của Rails, cũng như được sử dụng rất thường xuyên (cùng với .present?)
Theo như mô tả trong source code: một object được coi là blank nếu như nó false, empty hoặc là 1 chuỗi chỉ gồm các khoảng trắng.
Điều này cho phép ta dự đoán:
# những object dưới đây khi gọi `.blank?` sẽ trả về true vì chúng `empty`
[].blank? # => true
{}.blank? # => true
"".blank? # => true
# những object dưới đây khi gọi `.blank?` sẽ trả về true vì chúng `false`
false.blank? # => true
nil.blank? # => true
# những object dưới đây khi gọi `.blank?` sẽ trả về true vì chúng là chuỗi chỉ có khoảng trắng
" ".blank? # => true
"\s".blank? # => true
# như vậy, những object dưới đây nên trả về false khi gọi `.blank?`
true.blank? # => false
5.blank? # => false
Thử các lệnh trên trong rails console xác nhận các suy đoán trên là đúng.
Hãy cùng xem Rails định nghĩa .blank? ra sao (sao chép từ mã nguồn của Rails, đã lược bỏ chú thích):
class Object
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
end
class NilClass
def blank?
true
end
end
class FalseClass
def blank?
true
end
end
class TrueClass
def blank?
false
end
end
class Array
alias_method :blank?, :empty?
end
class Hash
alias_method :blank?, :empty?
end
class String
BLANK_RE = /\A[[:space:]]*\z/
def blank?
BLANK_RE === self
end
end
class Numeric
def blank?
false
end
end
Quên đi định nghĩa của .blank? trong lớp Object, xem phần còn lại ta sẽ thấy với Array và Hash thì .blank? chẳng qua là alias method của .empty?. Còn trong các lớp NilClass, FalseClass thì .blank? trực tiếp trả về false, trong các lớp TrueClass, Numeric thì .blank? trực tiếp trả về true.
Với lớp String, do có nhiều loại kí tự khoảng trắng (như space, tab) nên Rails sử dụng regex /[[:space:]]/ từ Ruby (xem Regexp). Điều này có nghĩa rằng tất cả các loại kí tự được Ruby xem là khoảng trắng sẽ được dùng cho việc xác định một chuỗi có gồm toàn khoảng trắng hay không, thể hiện qua ví dụ dưới đây:
# chuỗi này chỉ bao gồm toàn các kí tự khoảng trắng
"\t\n\r\s \u00a0".blank?
# => true
Vậy, như phần trên đã nói chỉ có Array, Hash và String có .empty?, trong khi .blank? trong các lớp trên đều đã bị khai báo đè lên trên khai báo của Object. Vậy tại sao .blank? trong Object vẫn được khai báo như sau:
class Object
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
end
Việc tìm hiểu ý nghĩa của đoạn mã này phụ thuộc vào bạn, mình sẽ tạm dừng câu chuyện về .blank? tại đây.
4. .present?
Nếu 1 object không blank, vậy nó present.
# An object is present if it's not blank.
#
# @return [true, false]
def present?
!blank?
end
Như vậy không có gì nhiều để nói về .present? nữa, tuy nhiên có một hàm có liên quan mật thiết đến .present? mà chúng ta cũng rất hay sử dụng, đó là .presence
def presence
self if present?
end
Với định nghĩa này ta có thể suy ra rằng .presence sẽ trả về object gọi nó nếu object đó present, còn không sẽ trả về nil.
Một ví dụ cho .presence:
s = " " || "default"
# => " "
s = " ".presence || "default"
# => "default"
5. presence
if user.name.blank?
name = "What's your name?"
else
name = user.name
end
name = user.name.presence || "What's your name?"
"".presence hoặc [].presence sẽ trả về nil.
name = ""
puts name.presence || "What's your name?" # => What's your name?
Ngoài ra còn có 1 ví dụ rất thú vị về presence như sau.
# News nếu có ít nhất là 1 news thì gửi mail và tweet
good_news = company.good_news
if good_news.count > 0
send_mail(good_news)
tweet(good_news)
end
Đoạn mã trên nếu dùng presence
if good_news = company.good_news.presence
send_mail(good_news)
tweet(good_news)
end
company.good_news trả lại kết quả là 0 thì câu lệnh company.good_news.presence sẽ trả về là nil. Khi đó câu lệnh if sẽ xử lý false.
Tương tự như thế, khi muốn kiểm tra điều kiện “trong trường hợp string có 1 giá trị nào đó”.
# Nếu name là nil hoặc là string trống ("") thì không hiện message lên
name = blog.user.name
if name.present?
show_message("Hello, #{name}!")
end
if name = blog.user.name.presence
show_message("Hello, #{name}!")
end
6. Dùng pluck thay vì map
pluck là method để lấy 1 column cho trước trong các record, mà không load toàn bộ các record đó. Vì thế mà tốc độ xử lý và RAM cũng hiệu quả hơn.
def admin_user_ids
User.where(admin: true).map(&:id)
end
def admin_user_ids
User.where(admin: true).pluck(:id)
end
7. Về timezone trong Rails
Trong Rails, có 2 cách để setting timezone, cách 1 là setting trong application.rb, cách 2 là sử dụng timezone dựa theo biến số môi trường TZ. Nếu trong trường hợp setting giữa 2 cách này mâu thuẫn với nhau, sẽ nảy sinh ra những lỗi không thể dự đoán trước. Vì thế, tốt hơn là thống nhất chỉ sử dụng timezone trong application.rb.
Ví dụ, không dùng Date.today mà dùng Date.current, không dùng Time.now mà dùng Time.current ( hoặc Time.zone.now )
7. Các method thời gian hay
Date.current # => Tue, 05 Nov 2013
Date.yesterday # => Tue, 04 Nov 2013
Date.tomorrow # => # => Tue, 06 Nov 2013
Date.current # => 2013-11-05
2.years.ago # => 2011-11-05 06:21:40 +0900
2.years.since # => 2015-11-05 06:21:40 +0900
2.months.ago # => 2013-09-05 06:21:40 +0900
2.months.since # => 2014-01-05 06:21:40 +0900
Weeks, days, hours, minutes, seconds cũng thế.
Ngoài ra còn rất nhiều cách viết khác nhau để lấy giá trị ngày tháng đặc biệt.
date = Date.current # => 2013-11-05
date.yesterday # => 2013-11-04
date.tomoroow # => 2013-11-06
date.prev_day # => 2013-11-04
date.next_day # => 2013-11-06
date.prev_day(2) # => 2013-11-03
date.next_day(2) # => 2013-11-07
date - 2.days # => 2013-11-03
date + 2.days # => 2013-11-07
date.ago(2.days) # => 2013-11-03
date.since(2.days) # => 2013-11-07
date.prev_month # => 2013-10-05
date.next_month # => 2013-12-05
date.prev_month(2) # => 2013-09-05
date.next_month(2) # => 2014-01-05
date - 2.months # => 2013-09-05
date + 2.months # => 2014-01-05
date.months_ago(2) # => 2013-09-05
date.months_since(2) # => 2014-01-05
date.ago(2.months) # => 2013-09-05
date.since(2.months) # => 2014-01-05
Week, year cũng thế.
date = Date.current # => 2013-11-05
date.beginning_of_month # => 2013-11-01
date.end_of_month # => 2013-11-30
date.beginning_of_day # => 2013-11-05 00:00:00 +0900
date.end_of_day # => 2013-11-05 23:59:59 +0900
datetime = Time.current # => 2013-11-05T06:43:53+09:00
datetime.beginning_of_hour # => 2013-11-05T06:00:00+09:00
datetime.end_of_hour # => 2013-11-05T06:59:59+09:00
date = Date.current # => 2013-11-05
date.tuesday? # => true
date.prev_week(:monday) # => 2013-10-28
date.next_week(:monday) # => 2013-11-11
8. Các method thay đổi string thành số nhiều, số ít, …
"my_book".camelize # => "MyBook"
"MyBook".underscore # => "my_book"
"my_book".dasherize # => "my-book"
"book".pluralize # => "books"
"person".pluralize # => "people"
"fish".pluralize # => "fish"
"book_and_person".pluralize # => "book_and_people"
"book and person".pluralize # => "book and people"
"BookAndPerson".pluralize # => "BookAndPeople"
"books".singularize # => "book"
"people".singularize # => "person"
"books_and_people".singularize # => "books_and_person"
"books and people".singularize # => "books and person"
"BooksAndPeople".singularize # => "BooksAndPerson"
"my_books".humanize # => "My books"
"my_books".titleize # => "My Books"
"my_book".classify # => "MyBook"
"my_books".classify # => "MyBook"
"my_book".tableize # => "my_books"
"MyBook".tableize # => "my_books"
II. Phong Cách Code Hay
1. Đặt if ở phía sau để rút gọn
if user.active?
send_mail_to(user)
end
send_mail_to(user) if user.active?
2. Dùng unless thay cho if + not
user.destroy if !user.active?
user.destroy unless user.active?
Tuy nhiên, nếu điều kiện phía sau unless phức tạp, có and, or, thì nên dùng if cho dễ hiểu.
user.destroy unless (user.active? || user.admin?) && !user.spam?
3. Sử dụng cách viết điều kiện thu gọn
if user.admin?
"I appreciate for that."
else
"Thanks."
end
user.admin? ? "I appreciate for that." : "Thanks"
Tuy nhiên, chỉ nên dùng cho những đoạn code có điều kiện đơn giản, không nên dùng với những đoạn code lồng nhau.
# khó đọc
user.admin? ? user.active? ? "I appreciate for that." : "Are you OK?" : "Thanks."
4. Dùng if đồng thời với phép gán
user = find_user
if user
send_mail_to(user)
end
if user = find_user
send_mail_to(user)
end
Tuy nhiên, cách viết này có thể gây hiểu nhầm cho người đọc: “lỗi type gõ thiếu dấu =, đáng ra phải là == hoặc != chứ”. Vì thế có người thích mà cũng có người không thích cách viết này.
5. Xác nhận điều kiện từ các class con
if parent.children
if parent.children.singleton?
singleton = parent.children.first
send_mail_to(singleton)
end
end
Viết gọn lại
if parent.children && parent.children.singleton?
singleton = parent.children.first
send_mail_to(singleton)
end
6. Không dùng return ở cuối method
Các ngôn ngữ khác phải cần, nhưng với ruby, cách viết không có return có vẻ được yêu thích hơn.
def build_message(user)
message = 'hello'
message += '!!' if user.admin?
return message
end
def build_message(user)
message = 'hello'
message += '!!' if user.admin?
message
end
7. Sử dụng Object#tap
def build_user
user = User.new
user.email = "hoge@hoge.com"
user.name = "Taro Yamada"
user
end
def build_user
User.new.tap do |user|
user.email = "hoge@hoge.com"
user.name = "Taro Yamada"
end
end
8.Khi ghép các chuỗi kí tự, không dùng “+” mà dùng “#{ }”
"Hello, " + user.name + "!"
"Hello, #{user.name}!"
9.Freeze các hằng số
freeze là cách khai báo một hằng số. Dùng cách này để tránh trường hợp hằng số bị thay đổi trong quá trình làm việc.
10. Chuỗi kí tự
CONTACT_PHONE_NUMBER = "03-1234-5678"
CONTACT_PHONE_NUMBER << "@#$%^"
puts CONTACT_PHONE_NUMBER # => 03-1234-5678@#$%^
CONTACT_PHONE_NUMBER = "03-1234-5678".freeze
CONTACT_PHONE_NUMBER << "@#$%^" # => RuntimeError: can't modify frozen String
11.Array
ADMIN_NAMES = ["Tom", "Alice"]
ADMIN_NAMES << "Taro"
puts ADMIN_NAMES # => ["Tom", "Alice", "Taro"]
ADMIN_NAMES = ["Tom", "Alice"].freeze
ADMIN_NAMES << "Taro" # => RuntimeError: can't modify frozen Array
12.Khi tạo 1 array, dùng %w( )、%i( ) thay cho []
Trường hợp muốn tạo 1 array các chuỗi kí tự, dùng %w( ) sẽ dễ viết và ngắn hơn.
actions = ['index', 'new', 'create']
actions = %w(index new create) # => ['index', 'new', 'create']
Từ Ruby 2.0 trở đi, có cách viết %i( ) để tạo symbol.
actions = %i(index new create) # => [:index, :new, :create]
13.Khi xử lý 1 array theo thứ tự, dùng “&:method” thay cho “object.method”
names = users.map{|user| user.name }
names = users.map(&:name)
Không chỉ có map mà cả each ,select hay các block khác đều có thể dùng cách viết này.
14.Khi khai báo 1 số lớn, thêm “_” vào sẽ dễ đọc hơn
ITEM_LIMIT = 1000000000
ITEM_LIMIT = 1_000_000_000
15.Dùng attr_reader thay cho những method getter đơn giản
class Person
def initialize
@name = "No name"
end
def name
@name
end
end
class Person
attr_reader :name
def initialize
@name = "No name"
end
# Không cần
# def name
# @name
# end
end
16.Trong 1 array mà mỗi phần tử mang 1 ý nghĩa khác nhau, có thể lấy vào nhiều biến cùng lúc
ans_array = 14.divmod(3)
puts "Thương #{ans_array[0]}" # => Thương 4
puts "Số dư #{ans_array[1]}" # => Số dư 2
quotient, remainder = 14.divmod(3)
puts "Thương #{quotient}" # => Thương 4
puts "Số dư #{remainder}" # => Số dư 2
Tương tự với method each của 1 hash.
# key và value được lấy về dưới dạng array
{name: 'Tom', email: 'hoge@hoge.com'}.each do |key_and_value|
puts "key: #{key_and_value[0]}"
puts "value: #{key_and_value[1]}"
end
# key và value được lưu vào 2 biến khác nhau
{name: 'Tom', email: 'hoge@hoge.com'}.each do |key, value|
puts "key: #{key}"
puts "value: #{value}"
end
17.Khi nối nhiều array với nhau, không dùng + mà nên dùng *(splat)
numbers = [1, 2, 3]
numbers_with_zero_and_100 = [0] + numbers + [100] # => [0, 1, 2, 3, 100]
numbers = [1, 2, 3]
numbers_with_zero_and_100 = [0, *numbers, 100] # => [0, 1, 2, 3, 100]
Trong trường hợp bạn quên mất dấu * thì kết quả sẽ như sau :
[0, numbers, 100] # => [0, [1, 2, 3], 100]
18. Cách viết ||= khi cần “nếu nil thì init”
def twitter_client
@twitter_client = Twitter::REST::Client.new if @twitter_client.nil?
@twitter_client
end
def twitter_client
@twitter_client ||= Twitter::REST::Client.new
end
19.Khi toàn bộ method là đối tượng của rescue thì lược bỏ begin/end
def process_user(user)
begin
send_to_mail(user)
rescue
# xử lý
end
end
def process_user(user)
send_to_mail(user)
rescue
# xử lý
end
20.Gọi rescue là StandardError thay vì Exception
Với những người đã từng học qua Java hay C# thì thường có thói quen sử dụng Exception để bắt lỗi ngoại lệ. Tuy nhiên, khi bắt Exception trong Ruby cũng đồng nghĩa với việc bắt luôn những lỗi cực kì nguy hiểm như NoMemoryError.
StandardError là 1 subclass của Exception để bắt lỗi thực hiện của code. rescue mặc định là sẽ bắt StandardError và các subclass của nó nên có thể bỏ qua khi viết code.
def process_user(user)
send_to_mail(user)
rescue Exception => ex
# bắt cả những lỗi nguy hiểm như NoMemoryError, ...
end
def process_user(user)
send_to_mail(user)
rescue => ex
# bắt tất cả các lỗi thực hiện (StandardError và các subclass của nó)
end
21.Khi thao tác với index, nêu dùng -1 thay vì size - 1
Code khi sử dụng size - 1
numbers = [1, 2, 3, 4, 5]
name = 'Viet on Rails'
numbers[numbers.size - 1] # => 5
name[name.size - 1] # => 's'
numbers[1..numbers.size - 2] # => [2, 3, 4]
name[1..name.size - 2] # => "iet on Rail"
Code khi sử dụng -1
numbers = [1, 2, 3, 4, 5]
name = 'Viet on Rails'
numbers[-1] # => 5
name[-1] # => 's'
numbers[1..-2] # => [2, 3, 4]
name[1..-2] # => "iet on Rail"
22.Find: trả lại yếu tố đầu tiên tìm thấy
def find_admin(users)
users.each do |user|
return user if user.admin?
end
nil
end
def find_admin(users)
users.find(&:admin?)
end
Sử dụng find_index nếu muốn trả về index của yếu tố đầu tiên tìm thấy.
23.Select: lấy tất cả những yếu tố thoả mãn điều kiện
def find_admins(users)
admins = []
users.each do |user|
admins << user if user.admin?
end
admins
end
def find_admins(users)
users.select(&:admin?)
end
Trái với select là reject, hàm này chỉ lấy những yếu tố false.
III. Một Số Tài Liệu Tham Khảo.
Bài viết chủ yếu là sưu tầm những ý hay nhằm chia sẻ kiến thức.
All rights reserved