Sau quãng thời gian dài làm việc với Ruby on Rails ở nhiều dự án khác nhau, mình đã đúc kết được một số kinh nghiệm coding mà nếu áp dụng theo, nó có thể giúp cho code của bạn dễ đọc và dễ dàng bảo trì hơn.

Naming Convension

Readable and meaningfull

Việc rút gọn tên biến có thể làm cho đoạn code trở nên ngắn gọn nhứng bộ não của chúng ta lại dễ dàng ghi nhớ những từ có nghĩa hơn. Vì vậy, nếu bạn đặt tên biến là một từ vô nghĩa thì khi đọc code, bạn sẽ có cảm giác không thoải mái và khó hiểu:

Bad:

posts.each_with_object([]) do |p, r|
  next if p.draft?
  r << p.as_json.merge(user_name: p.user.name)
end

Good

posts.each_with_object([]) do |post, results|
  next if post.draft?
  results << post.as_json.merge(user_name: post.user.name)
end

Ignore current context

Không nên thêm current context khi đặt tên cho variables, methods vì nó không cần thiết.

Bad:

class User
  def initialize user_name
    @user_name = user_name
  end

  def user_valid?
    user_name.present?
  end
    
  private
  attr_reader :user_name
end

user = User.new("Lam")
user.user_valid?

Good:

class User
  def initialize name
    @name = name
  end

  def valid?
    name.present?
  end
    
  private
  attr_reader :name
end

user = User.new("Lam")
user.valid?

Not be too detailed

Cái gì quá cũng không tốt và việc đặt tên methods hay class cũng vậy:

Bad:

class UpdateUserSalaryAtEndOfMonth
  # Logic update salary at end of month
end

Chỉ cần nhìn vào tên class chúng ta đã có thể biết được nội dung bên trong class đó là gì:

  • Action update user salary
  • Thời điểm update là ngày cuối tháng

Nhưng có một vấn đề đó là action update user salary có thể sẽ không thay đổi nhưng thời gian update thì hoàn toàn có thể. Đến lúc đó, tên class sẽ phải thay đổi theo để phù hợp với yêu cầu và như thế thì không tốt chút nào.

Vì vậy, tên của methods hay class chỉ nên chứa đựng những thông tin ít thay đổi của biến hay class đó và không cần quá chi tiết:

Good:

class UpdateUserSalary
  # Logic update salary at specific time 
end

Not contains values

Mục đích của việc sử dụng constant là để khi thay đổi, ta không phải tìm và sửa ở nhiều nơi, chứ không phải là để dùng constant đó ở nhiều nơi.

Bad:

PER_PAGE_20 = 20

MINUTES_PER_HOUR = 60

Hãy tưởng tượng, sẽ thế nào nếu một ngày bạn muốn thay đổi giá trị per page lên thành 30 ở những nơi đang dùng PER_PAGE_20?

Good:

DEFAULT_PER_PAGE = 20

COOKIE_EXPIRATION_IN_MINUTES = 60

Use by context not by value

Một trường hợp khác mà chúng ta thường mắc phải:

Bad:

class User < ApplicationRecord
  PERMITTED_ATTRS = %i(name email status_id)
end
class UsersController < ApplicationController
  def index
    render json: @users.as_json(User::PERMITTED_ATTRS)
  end

  def create
    @user = User.create(user_params)
  end
    
  private
  def user_params
    params.require(:user).permit(User::PERMITTED_ATTRS)
  end
end

Mọi thứ sẽ hoạt động bình thường nếu dữ liệu trả về ở index giống với các thông tin permit khi tạo user.

Nhưng rõ ràng, khi chỉ muốn thay đổi logic tạo user, việc sửa User::PERMITTED_ATTRS tự nhiên sẽ làm ảnh hưởng đến cả index mặc dù chúng không hề liên quan đến nhau.

Good:

class User < ApplicationRecord
  PERMITTED_ATTRS = %i(name email status_id)
  JSON_ATTRS = %i(name email status_id)
end
class UsersController < ApplicationController
  def index
    render json: @users.as_json(User::JSON_ATTRS)
  end

  def create
    @user = User.create(user_params)
  end
    
  private
  def user_params
    params.require(:user).permit(User::PERMITTED_ATTRS)
  end
end

Methods

Use keyword arguments

Method chứa càng ít tham số đầu vào thì càng tốt. Tuy nhiên điều này không phải lúc nào cũng thực hiện được. Vì thế, trong trường hợp method có chứa nhiều tham số, hoặc có những tham số là optional, hãy sử dụng keyword arguments.

Bad:

def send_mail email, cc = [], reply_to = []
  mail_to(email, cc, reply_to)
end

send_mail("example@mail.com", [], ["reply@mail.com"])

Good:

def send_mail email, cc: [], reply_to: []
  mail_to(email, cc, reply_to)
end

send_mail("example@mail.com", reply_to: ["reply@mail.com"])

Raise error if needed

Nếu method nhận tham số được truyền từ client và nó có thể là bất cứ giá trị nào, thì việc kiểm tra để tránh các ngoại lệ không mong muốn là cần thiết:

Good:

def role_name_for role_id
  return "" if [:normal, :member].exclude?(role_id)

  User.send("role_#{role_id}_text")
end

role_name_for(params[:role_id])

Tuy nhiên, với những tham số được truyền vào bởi chính lập trình viên thì việc kiểm tra là không cần thiết. Nếu có lỗi thì đó có thể được xem là lỗi typo và method cũng nên raise lỗi trong trường hợp này.

Bad:

def role_name_for role_id
  return "" if [:normal, :member].exclude?(role_id)

  User.send("role_#{role_id}_text")
end

role_name_for(:normal)

Good:

def role_name_for role_id
  User.send("role_#{role_id}_text")
end

role_name_for(:normal)

Define method in order

Thông thường, chúng ta sẽ đọc code từ trên xuống dưới,
vì vậy methods cũng nên được đặt theo thứ tự này để dễ dàng folow hơn.

Bad:

def perform
  save_user
  send_mail_confirm
end

def send_mail_confirm
end

def build_user
end

def save_user
  build_user
  validate_user!
end

def validate_user!
end

Good:

def perform
  save_user
  send_mail_confirm
end

def save_user
  build_user
  validate_user!
end

def send_mail_confirm
end

def build_user
end

def validate_user!
end

Explicit instance variables initialization

Instance variables có thể được khởi tạo và sử dụng ở bất cứ đâu trong class. Vì vậy, nếu không được khai báo rõ ràng, sẽ rất khó để có thể theo dõi chúng khi chạy code:

Bad:

def save
  validate!
  @user.save
end

def validate!
  @user = User.new(user_params)
  raise("User invalid") if @user.invalid?
end

def user_params
end

Khi đọc method save chúng ta hoàn toàn không biết @user được tạo ra ở đâu và khi nào điều này làm cho đoạn code trở nên không rõ ràng.

Good:

def initialize
  @user = User.new(user_params)
end

def perform
  validate!
  save
end

def validate!
  raise("User invalid!") if @user.invalid?
end

def save
  @user.save
end

def user_params
end

Conclusion

Vừa rồi là một số tips viết code Ruby tốt hơn. Hi vọng bài viết sẽ hữu ích với bạn.