+5

Rails Service Object

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ác keys của hash sẽ gọi được bằng cả 2 cách :foo hoặc "foo"
  • symbolize_keys để trả về 1 hash mới có keys được convert về dạng symbol cùng với việc khai báo thêm attr_reader nhằm giúp việc sử dụng params truyền vào ở trong các private 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí