Design Pattern và các Object cơ bản trong dự án API với Rails - P1
Bài đăng này đã không được cập nhật trong 4 năm
Design pattern là gì?
Nếu bạn đã từng code, có lẽ việc cảm thấy thích thú khi tìm ra giải pháp cho những vấn đề được giao đã không còn gì xa lạ. Trên con đường đó, có những tình huống bạn phải đối mặt không chỉ một lần rồi suy nghĩ về nó và đi đến kết luận "again and again"... Vậy tại sao không viết lại "solution" để áp dụng khi cần thiết? Từ đó khái niệm về design pattern xuất hiện, tập hợp mọi "solution" của lập trình viên có hiểu biết chuyên sâu chia sẻ được mọi người ứng dụng trong quá trình thiết kế và xây dựng dự án, bên cạnh đó việc biết đến design pattern giúp bạn tiết kiệm thời gian và "effort" trong công việc.
Các Object cơ bản trong dự án API với Rails
Để nói về design pattern với Rails, bài viết này mình sẽ follow bố cục 3 câu hỏi chính where? - Ảnh hưởng ở đâu?, why? - Tại sao phải sử dụng? và how? - Sử dụng như thế nào?
Where?
Làm việc với Rails hẳn bạn đã quen với MVC (Model - View - Controller) và với API chỉ có Model - Controller, từ đó việc áp dụng design pattern trong Rails API xảy ra ở Model và Controller.
Why?
Ngán ngẩm khi làm việc với hơn nghìn dòng code cùng trên một file, đôi khi comment trong code còn không xuất hiện, đó là lúc bạn nhận ra phải đi debug từng dòng để test xem những fuction đó hoạt động như thế nào và trả về những gì. Để tìm được thông tin cần thiết khi làm việc hay bảo trì chả khác gì việc đi mò kim đáy bể , đặc biệt với một member mới tham gia dự án. Để phân tích rõ hơn mình sẽ chia ra làm 2 phần riêng biệt là Model và Controller.
Đối với Model:
- Trước tiên về phần validates khi tạo form:
Form được tạo ra để create/validate record dựa vào model_params hẳn đã không còn xa lạ nữa, việc khá là đơn giản nếu bạn chỉ xử lý trên một model nhất định. Tuy nhiên, không phải lúc nào mọi việc cũng diễn ra như ý muốn, một form có thể create/validate nhiều record và nhận attributes params tương ứng với record đó.
Đến đây hẳn nhiều bạn đã nghĩ đến việc sử dụng accepts_nested_attributes_for: của Rails, như vậy nếu form chứa nhiều model cùng làm việc với nhau trong điều kiện riêng rẽ cho đối với mỗi model (giả sử model Company khi :update, model Tag khi :new), số câu lệnh accepts_nested_attributes_for: tăng lên, gián tiếp làm phình to model và một lý do nữa là một phần những lập trình viên lâu năm cho rằng việc dùng accepts_nested_attributes_for: là không tốt và sẽ được loại bỏ khỏi Rails sau này.
Sau khi mình lục tìm trong bách khoa toàn thư desgin pattern, chúng ta có một object khá thú vị.
FORM OBJECT
Form object là kỹ thuật tạo một class ở đó tất cả những model bị tác động đến trong quá trình tạo form được đặt vào cùng chỗ và xử lý validate trong một form duy nhất. Giả dụ khi xác nhận lại thông tin một user lúc nhấn vào link invite từ hệ thống, bạn phải xác nhận cả thông tin của công ty mà user làm admin.
class Api::V1::ConfirmationForm
include ActiveModel::Model
attr_accessor :name, :phone_number, :token, :company_name, :postal_code, :address_building
validates :name, :phone_number, :token, :company_name, presence: true # validates từ ActiveModel::Model
def save
raise Api::Error::ControllerRuntimeError, :invalid_params unless valid?
# Hàm valid? trong ActiveModel::Model, return false khi vi phạm validates ở trên
ActiveRecord::Base.transaction do
create_company
create_user
end
end
private
def create_company
#Tạo một company với các attributes thích hợp
end
def create_user
#Tạo một user
end
end
Ở controller ta gọi form như sau:
class Api::V1::ConfirmationsController < Api::V1::BaseController
def create
render_jsonapi Api::V1::ConfirmationForm.new(confirmation_params).save, type: :user_confirmation
end
private
def confirmation_params
params.require(:confirmation).permit :name, :phone_number, :token, :company_name, :postal_code, :address_building
end
Lúc này bạn có thể giảm bớt các câu validates bên model User và Company, làm model trở nên rõ ràng hơn thuận tiện cho việc maintain và sử dụng. Vậy là xong phần thứ nhất khi xử lý model, phần hai sẽ liên quan đến việc tóm gọn scope.
- Trong scope:
Trong dự án mình từng tham gia, ở model User đã từng xuất hiện số lượng dòng code ~200, và không nói quá thì trong nó như một bài "văn" vậy rất rối mắt và trong đó đa số các line code bị chiếm dụng bới scope . Giả sử bạn muốn lấy điểm cao nhất của từng users từ bảng users_exams và sắp xếp nó.
scope :sort_by_max_score, (lambda do |direction|
left_joins(:users_exams)
.select("(select max(score) from users_exams
where users_exams.user_id = users.id) as max_score")
.order("max_score": direction, updated_at: :desc)
end)
scope :distinct_users, -> {select("distinct users.*")} # Câu distinct ở đây bởi với mỗi user có nhiều users_exams
User.distinct_users.sort_by_max_score
Vậy là được một câu sort , thử nghĩ nếu có cả filter và không chỉ mỗi max_score mà còn blah blah các thứ, vậy thì chúc mừng bạn sắp phải viết bài văn vào model User rồi đấy. Nhưng may thay design pattern cho chúng ta một object để làm tất cả những công việc query phức tạp như thế này.
QUERY OBJECT
Query Object theo trường hợp lý tưởng có thể hiểu là một class riêng biệt chứa một câu truy vấn cụ thể, và chỉ thực hiện một logic duy nhất.
class UsersQuery
def initialize users = User.all, sort_params = {}, q_params = {}
@users = users
@sort_params = sort_params
@q_params = q_params
end
def all
users.from("(#{QUERY_OBJ}) as users").ransack(q_params).result
end
def sort
all.send("sort_by_#{key}", direction)
end
private
attr_reader :users, :sort_params, :q_params
QUERY_OBJ = <<-SQL
# Raw SQL
SQL
def key
sort_params.present? ? sort_params.keys.first : :updated_at
end
def direction
sort_params.present? ? sort_params.values.first : :desc
end
end
def Api::V1::UsersController < Api::V1::BaseController
def index
users = UsersQuery.new.sort
render_jsonapi users
end
end
Như vậy bạn có thể thêm bao nhiêu câu query vào trong QUERY_OBJ tùy thích và biến cái scope phức tạp như lúc đầu thành thế này:
scope :sort_by_max_score, ->(direction) {order max_score: direction, updated_at: :desc}
Một query_object được sử dụng tuân thủ theo các quy tắc về Single Responsibility Principle, trả về ActiveRecord::Relation(không phải Array) và đại diện cho một câu truy vấn phức tạp.
Bài viết đến đây là hết rồi, chúc bạn thành công trong công cuộc khiến code của mình clean và dry. Hẹn gặp lại ở phần 2 với design pattern cho Controller và một vài lỗi anti-pattern thông dụng
Bài viết tham khảo từ 2 cuốn sách: Head First Design Pattern và Design Patterns for Dummies
All rights reserved