Rails Service Object
Bài đăng này đã không được cập nhật trong 3 năm
Tổng quan về Rails Service Object
Trong các dự án lớn sử dụng Rails với mô hình MVC, chắc chắn sẽ có những yêu cầu nghiệp vụ phức tạp, bao gồm các chức năng đòi hỏi chúng ta phải tương tác với rất nhiều các Model khác nhau, không nhắm đến 1 model cụ thể, dẫn đến việc kiểm soát (phát triển và bảo trì) các chức năng này của hệ thống sẽ rất khó khăn.
Service Object
chính là một trong những giải pháp cho vấn đề này. Nó được sinh ra như một nơi chứa các business logic khác nhau mà trong đó đối tượng xử lí không hoàn toàn thuộc về model nào.
Lợi ích của Service Object
là nó giúp chúng ta tập trung toàn bộ logic các chức năng vào trong 1 object riêng biệt mà không cần phải chia nhỏ nó vào các controller và model như cách thông thường. Lúc nào cần dùng đến thì chúng ta mới gọi object đó ra. Chính vì khối logic được tập trung hết vào trong 1 object nên sẽ tối giản controller và model đi rất nhiều, code clean và quá trình maintain sau này cũng đỡ vất vả hơn.
Nguyên tắc hoạt động của Service Object
cũng không quá phức tạp, bao gồm:
- Nhận các input đầu vào (có thể là single value, hash of values, JSON...)
- Thực hiện logic các chức năng
- Trả về các kết quả ở output (có thể là boolean value, ActiveRecord object, status object...)
Sử dụng
Thông thường thì trong Rails các services của hệ thống sẽ được tập trung ở trong thư mục app/services
. Tùy vào tính đặc thù và độ phức tạp của dự án mà chúng ta có thể chia nhỏ services ra thành nhiều mục con.
Giả sử như hệ thống của chúng ta chỉ thực hiện 1 chức năng đơn giản là format lại tên và email người dùng nhập vào chẳng hạn, thông thường, chúng ta có thể xử lí hết ở controller:
class UsersController < ApplicationController
.
.
.
def index
@result = input_format(params[:name], params[:email])
end
private
def input_format param_1, param_2
params_1 + " - " + params_2
end
end
Bây giờ, yêu cầu bài toán cần thêm nhiều kiểu format hơn, đi cùng với đó là nhiều kiểu params nhập vào hơn, không chỉ mỗi name với email nữa, mà là thêm ngày tốt nghiệp, ngôn ngữ đang theo học, bằng cấp, chứng chỉ... thì nếu chúng ta cứ tiếp tục dồn nhét hết đống logic cồng kềnh đó vào controller như cách lúc đầu, chẳng mấy chốc mà controller của chúng ta sẽ trở nên khổng lồ với cả chục function và trăm dòng code. Tất nhiên là sau đấy sẽ chẳng có ai muốn mò lại vào controller này nữa.
Vậy nếu chúng ta áp dụng Service Object
vào trường hợp này thì sao?
Đầu tiên, chúng ta cần xác định xem những function nào sẽ được nhóm vào cùng 1 service: ở đây sẽ là các dạng format cho input của người dùng nhập vào. Chúng ta sẽ cho hết các format function vào chung 1 service với cái tên là UserFormatService
chẳng hạn:
class UserFormatService
FORMAT_DATA_METHODS = %i(name_format graduation_format language_format address_format
skill_format)
UNDERSCORE = " _ "
def initialize params
@params = ActiveSupport::HashWithIndifferentAccess.new params.symbolize_keys
end
def input_format
FORMAT_DATA_METHODS.each{|method| send(method)}
end
private
attr_reader :params
def name_format
return unless (params[:name] && params[:email])
params[:name] + UNDERSCORE + params[:email]
end
def graduation_format
return unless params[:graduated_at]
"graduated at:" + params[:graduated_at]
end
.
.
.
end
Cấu trúc bên trong của 1 service, thông thường sẽ bao gồm hàm khởi tạo initialize
và hàm perform (ở đây đang là input_format
) và các private methods do chúng ta tự định nghĩa cho hàm perform.
Ở đây, chúng ta nhóm hết các input format vào trong 1 constant, rồi sau đó sẽ gọi trong hàm perform input_format
tương ứng với các private methods cùng tên được định nghĩa bên dưới.
Trong hàm khởi tạo initialize
, chúng ta dùng:
ActiveSupport::HashWithIndifferentAccess
để bảo đảm việc cáckeys
củahash
sẽ gọi được bằng cả 2 cách:foo
hoặc"foo"
symbolize_keys
để trả về 1hash
mới cókeys
được convert về dạngsymbol
cùng với việc khai báo thêmattr_reader
nhằm giúp việc sử dụng params truyền vào ở trong cácprivate methods
ở bên dưới được thuận tiện hơn.
Ok, vậy là xong phần build service, giờ chúng ta sẽ xem xét đến việc dùng nó ở đâu. Service cũng giống như 1 lớp ranh giới tương quan giữa controller và model, bản thân nó cũng có thể tương tác với DB. Trường hợp này chúng ta cũng có thể gọi nó trong controller:
class UsersController < ApplicationController
def index
@result = UserFormatService.new(params).input_format
end
end
Ở đây chúng ta làm cả 2 việc cùng lúc là khởi tạo UserFormatService
và gọi đến hàm input_format
của nó luôn. Sau đó thì chỉ việc xử lý @result
ở phía view sao cho phù hợp thôi.
Vậy là chúng ta đã cơ bản áp dụng được Service Object
cho hệ thống rồi, nhìn chung thì số dòng code thực tế tuy không thay đổi, nhưng lợi ích mang lại ở đây là khá lớn:
- Các tác vụ được gộp chung vào 1 service được định nghĩa rõ ràng, rất dễ dàng cho việc đọc, maintain hoặc thay đổi sau này
- Code của chúng ta clean và tường minh hơn
- Khả năng tái sử dụng
- Dễ dàng cho việc viết các test case
- ...
Tổng kết
Bài viết nhằm chia sẻ một chút kiến thức của mình và cách sử dụng cơ bản về Rails Service Object
, bài viết còn nhiều hạn chế, cảm ơn các bạn đã đọc.
Tài liệu tham khảo:
https://www.engineyard.com/blog/keeping-your-rails-controllers-dry-with-services
http://www.thegreatcodeadventure.com/service-objects-in-rails/
All rights reserved