+13

Một Số Method Và Phong Cách Code Hay Trong Rails

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 = "[email protected]"
  user.name = "Taro Yamada"
  user
end
def build_user
  User.new.tap do |user|
    user.email = "[email protected]"
    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 # => [email protected]#$%^
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: '[email protected]'}.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: '[email protected]'}.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

Viblo
Let's register a Viblo Account to get more interesting posts.