Tổng quan Service Object trong rails

Hi, chào các bạn, sau 1 chuỗi serial các bài về xử lý big data bằng spark giờ mình sẽ trở lại đề tài quen thuộc đó là ruby on rails 😃. Lần này mình sẽ giới thiệu đến các bạn 1 hình thức viết code khá mới cho rails app đó là Service object. Chắc nhiều bạn sẽ tò mò về vấn đề này vì cái này ở tutorial ko thấy nói. OK thực chất cái này được sinh ra dựa trên nhu cầu xử lý rất nhiều logic mà đối tượng được xử lý lại ko thuộc 1 model nào cả. E hèm nói kiểu này có thể khó hiểu nhưng các bạn có thể hình dung (hoặc đã gặp phải) đó là mình cần viết rất nhều code xử lý 1 tác vụ nào đó ví dụ down load file, đọc file rồi xử lý bla bla gì đó. Và thường thì đối với các tác vụ không liên quan đến model thì chúng ta sẽ viết vào trong controller để xử lý. Và điều này gây ra 1 vấn đề đó là controller phình code 1 cách đáng kể. Mình đã từng gặp vài dự án có 1 controller mà số line code từ 1k đến 2k dòng. Tin mình đi, lúc đấy thực sự khá là nản khi đọc code để tối ưu hoặc fix lỗi, chưa kể ko có comment nên đọc xong 1 block code mà mình vẫn chưa thể hình dung rằng đoạn code này nó code chạy cái gì. Thế là phải đọc đi đọc lại, nhập thử dữ liệu để test .... nói chung là rất tốn thời gian và ... mệt. Do đó chúng ta cần 1 thứ có thể dọn đống logic đó vào 1 chỗ để sử dụng ở controller hoặc ở bất cứ đâu bạn muốn. Và đó là lúc service object ra đời. Nôm na Service Object giúp chúng ra tạo ra 1 object để xử lý 1 chuỗi các logic, do đó thay vì khi báo 1 đống code trong controller ta chỉ cần 1 dòng code gọi object service ra để xử lý. Thôi để dễ hiểu hơn chúng ta sẽ bắt tay vào làm 1 ví dụ đơn giản để có thể hiểu hơn. Tất cả các Service Object sẽ đc lưu riêng ở thư mục service, vì lúc đầu khi khởi tạo rails ko có thư mục service nên chúng ta cần tạo bằng tay.

mkdir app/service

Tùy thuộc vào độ phức tạp của dự án mà bạn có thể tạo thêm các sub folder để chia service ra cho dễ quản lý. Cơ mà cái này tính sau ông bà đã dạy cái nào dễ thì làm trước, khó thì làm sau. Cứ dễ mà làm trước đã.

Ví dụ trang web của bạn cho phép người dùng tính tổng của 2 số nhập vào( kể cả nhập chữ cũng vẫn cộng =)) ). Thì hồi xưa ta sẽ khai báo hết trong controller như sau

class EasyMathsController < ApplicationController
  def index
    @result = awsome_sum(params[:number_one], params[:number_two])
  end
  
  private
  def awsome_sum number_one, number_two
    number_one.to_f + number_two.to_f
  end
end

Sau đó ở view ta sẽ kết quả @result cho user Rồi, chắc nhiều bạn cho rằng thế này vẫn bình thường cứ để trong contrller đi có sao đâu mà lại đẻ ra thằng service object làm gì? Ok nếu chỉ có vậy thì đơn giản quá rồi code xong đi chơi thôi. Tuy nhiên sau này khách hàng yêu cầu ở view đó người dùng có thể làm nhiều cái khác nữa, ví dụ như là làm phép trừ, nhân, chia, khai căn, bình phương .... abc và xyz. Không sao cả cứ như logic trên mỗi yêu cầu sẽ viết 1 hàm để xử lý và thế là controller của bạn sẽ dần dần phình to lên chục dòng, trăm dòng và có thể là cả nghìn dòng code ... và từ đó bạn ko muốn sờ đến cái controlelr này nữa (bye)

Đó là lúc để cho Service Object thể hiện

ta sẽ viết lại con troller như sau

class EasyMathsController < ApplicationController
  def index
    @result = CrazyMathService.new(params).perform
  end
end

Ngạc nhiên chưa, tất cả đống xử lý chỉ gói gọn lại trong 1 dòng code CrazyMathService.new(params).perform và đó chính là service object Kế tiếp chúng ta cần khai báo cho thằng service object này

vim app/service/easy_math_service.rb

Sau đó copy đống code này vào

class EasyMathService
  def initialize(params)
    @number_one = params[:number_one]
    @number_two = params[:number_two]
    @operator = params[:operator]
  end
  
  def perform
    check_condition_math
    do_math
  end
  
  private
  def check_condition_math
  # check some condition for math
  end
  
  def do_math
    case @operator
    when "add"
      #do sth
    when "sub"
      #do sth
    when "mutiple"
    .....
    else
      "invalid operator"
    end
  end
end

Đại khái code bạn sẽ là vậy. ở Đây mình sẽ giải thích chi tiết hơn quá trình xử lý của đống code này. Đầu tiên là ở controller mình có dòng code

@result = CrazyMathService.new(params).perform

thức tế đoạn code này sẽ làm 2 phần chính Bước 1 là khởi tạo ra 1 service object với thông tin params truyền vào

CrazyMathService.new(params)

Bước 2 là gọi hàm tính toán theo yêu cầu đề bài dựa trên các thông số đã lưu trong object mà ta khởi tạo ở trên

Tổng quan về lượng code là hầu như không đổi nhưng có 1 điểm hay ở đây là ta đã chia nhỏ được các tác vụ để có thể gọi dùng một các độc lập. Và lợi ích thu được (theo mình là rất lớn)

  1. Code nhìn sẽ trông ngắn hơn đọc dễ hơn
  2. Code tường minh hơn khi ta chia nhỏ các service và đặt tên cho chúng
  3. Khả năng tái sử dụng tốt hơn dễ dàng hơn
  4. Khả năng maintain, debug về sau này dễ dàng hơn cách viết chung rất nhiều
  5. Viết test case cũng sẽ tập trung và dễ dàng hơn

Ngoài việc sử dụng ở controller ta có thể gọi service ở mọi chỗ khác nếu cần dùng đến.

Giờ mình sẽ lấy 1 ví dụ khác để bạn có thể hiểu rõ hơn Giả sử khách hàng muốn chúng ta làm một chức năng đó là xác thực user với 1 bên thứ 3 nào đó ví dụ như facebook chẳng hạn sau đó lấy thông tin bạn cho phép để đăng ký 1 tài khoản rồi gửi mail để yêu cầu bạn xác nhận.

Xử lý trên sẽ bao gồm 2 bước cơ bản: Đầu tiên đó là dựa vào token mà facebook trả về để lấy thông tin Bước 2 là đăng ký user và gửi mail xác thực

#app/service/authorization_user_facebook_service
class AuthorizationUserFacebookService
  def initialize(token)
    @token = token
    @user = nil
  end
  
  def perform
    create_new_user_facebook
    send confirm_mail
  end
  
  private
  def create_new_user_facebook
    # lấy thông tin user từ facebook dựa vào token
    user_attributes = get_facebook_info @token
    # tạo user và lưu vào trong data base
    @user = User.create user_attributes
  end
  
  def confirm_mail
    #tạo email confirm cho người dùng
     mail = UserMailer.send_confirm_email @user
     #gửi mail đi
    mail.deliver
  end
end

Chắc đến giờ thì chắc bạn đã hiểu được phần nào công dụng của service rồi, nói chung đây là 1 giải pháp chưa chính thức của rails (tuy nhiên đang nhiều người sử dụng và được đánh giá rất tốt). Nó giúp cho hệ thống của bạn sạch sẽ hơn, các controller xử lý logic rõ ràng hơn. Tuy nhiên đây chỉ là 1 giải pháp trơ giúp bạn mà thôi, hệ thống của bạn có sạch đẹp được hay không vẫn là phần lớn do các bạn tổ chức file do đó trước khi tạo bất cứ chức năng gì mới bạn nên suy sét kĩ về khả năng sử dụng, mở rộng, vị trí và vai trò nó trong dự án. Bài viết của mình đến đây là hết xin hẹn gặp lại các bạn trong bài viết sau 😉