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:**
```ruby
posts.each_with_object([]) do |p, r|
next if p.draft?
r << p.as_json.merge(user_name: p.user.name)
end
```
**Good**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
class User < ApplicationRecord
PERMITTED_ATTRS = %i(name email status_id)
end
```
```ruby
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:**
```ruby
class User < ApplicationRecord
PERMITTED_ATTRS = %i(name email status_id)
JSON_ATTRS = %i(name email status_id)
end
```
```ruby
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:**
```ruby
def send_mail email, cc = [], reply_to = []
mail_to(email, cc, reply_to)
end
send_mail("example@mail.com", [], ["reply@mail.com"])
```
**Good:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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:**
```ruby
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.