Service Objects - Giải pháp cho các nghiệp vụ phức tạp trong Rails

76873.jpg

MVC(Model - View - Controller) - là mô hình rất khoa học và là ưu điểm nổi bật của Rails. Các thư mục được cấu trúc theo mô hình MVC giúp các nhà phát triển dễ dàng kiểm soát được ứng dụng của mình.

Nhưng khi phát triển một ứng dụng có quy mô lớn thì sao : sẽ có nhiều yêu cầu nghiệp vụ phức tạp phát sinh, các Action thực hiện có thể tương tác với các service bên ngoài khác, nó có thể không thuộc một Model chính nào cả và động tới nhiều Model khác nhau... Dẫn tới việc kiểm soát các nghiệp vụ này trở nên rất khó khăn.

Service Object là giải pháp sẽ giúp ta kiểm soát vấn đề này. Để thực hiện, ta sẽ thêm một lớp chỉ để xử lý các Action liên quan tới nghiệp vụ phức tạp đó thay vì đẩy nó vào ActiveRecord của Rails, điều này được gọi là PORO(Plain Old Ruby Object).

Để tường minh hơn ta cùng xét một ví dụ sau:

Một ứng dụng cung cấp dịch vụ bán vé cho người dùng, yêu cầu đầu tiên là sau khi người dùng chọn mua vé họ sẽ nhận được vé qua email:

# app/controllers/tickets_controller.rb #create

   if @ticket.save
     Email.send_email_to_user(current_user.email, @ticket)
     # code
   else
     # code
   end

Mọi việc vẫn diễn ra tốt đẹp cho tới một ngày bạn nhận được thêm yêu cầu sau từ phía khách hàng:

"Hãy gửi SMS thông tin vé vào số điện thoại của người dùng nếu chúng tôi có số điện thoại của họ?"

Để giải quyết vấn đề này bạn có thể sẽ nghĩ tới 2 lựa chọn sau:

  • Một là thêm một điều kiện trong tickets_controller.rb và gọi chức năng gửi tin nhắn SMS. Tuy nhiên, điều này đi ngược lại triết lý của Rails (Fat model, Thin controller) nếu khách hàng có thêm yêu cầu gửi thông tin qua nhiều phương tiện khác nữa.

  • Hai là đưa chức năng gửi vé qua email và số điện thoại vào một phương thức được định nghĩa trong model User:

# app/models/user.rb
  class User < ActiveRecord::Base
    # code

    # this method is called from the controller
    def send_notifications(ticket)
      Email.send_email_to_user(self.email, ticket)
      Sms.send(self.mobile, ticket) if self.mobile
    end
  end

Bây giờ mỗi khi khách hàng muốn thêm 1 yêu cầu gửi qua 1 phương tiện khác bạn sẽ chỉ cần thêm vào hàm send_notifications là đủ.

Nhưng một thời gian sau khách hàng của bạn lại có thêm yêu cầu : trừ tiền vé trong tài khoản của người dùng sau khi gửi email xác nhận, thay đổi lịch, giờ vé nếu có yêu cầu của người dùng và thông báo qua email, chỉ gửi thông báo bằng email nếu người dùng chấp nhận nhận vé qua email, SMS.... Các yêu cầu này luôn thay đổi theo thời gian vì đó là logic kinh doanh.

Giờ đây nếu tiếp tục lựa chọn cách thêm Action vào model để giải quyết yêu cầu thì chẳng mấy mà model của bạn sẽ trở lên cồng kềnh và bạn cũng chẳng muốn mở lại mà maintain đâu!

Thêm nữa bạn sẽ nhận thấy rằng :

  • Mặc dù các chức năng được gửi đến từng người sử dụng khi họ có yêu cầu tới dịch vụ, nhưng nó không phải chức năng cốt lõi của model User.

  • Việc kiểm tra các mô hình, cấu trúc các thư mục sẽ trở lên khó khăn.

Bạn thấy sao nếu chúng ta gộp các logic kinh doanh ấy vào một mô hình mà tạm gọi là dịch vụ theo ứng dụng, nó sẽ chứa toàn bộ yêu cầu dịch vụ phát sinh của khách hàng, và bạn có thể dễ dàng kiểm soát cũng như thay đổi chúng mà không phải tìm trong model cồng kềnh kia nữa.

gHQtrsr.png

Để Rails hiểu mô hình ta mới thêm khi khởi chạy ứng dụng ta cấu hình như sau :

# config/application.rb
module <Rails Application Name>
  class Application < Rails::Application
    # code
    config.autoload_paths << Rails.root.join('services')
    # code
  end
end

Và việc xửa lý các yêu cầu mới sẽ được thêm như thế này :

# Sending notifications to users after a ticket has been purchased
class UserNotificationService
  def initialize(user)
    @user = user
  end

  # send notifications
  def notify(ticket)
    Email.send_email_to_user(@user.email, ticket)
    Sms.send(@user.mobile, ticket) if @user.mobile
  end
end

Trong controller bạn chỉ việc gọi dịch vụ này để thực hiện :

# app/controllers/tickets_controller.rb #create

   if @ticket.save
     UserNotificationService.new(current_user).notify(@ticket)
     # code
   else
     # code
   end

Nhưng làm sao để kiểm tra và kiểm thử chúng???

Bạn hãy viết Rspec theo cấu trúc sau :

1.png

require 'rails_helper'

describe UserNotificationService do
  let(:user) { FactoryGirl.create(:user, mobile: mobile) }
  let(:ticket) { FactoryGirl.create(:ticket) }

  subject(:notification) do
    UserNotificationService.new(user).notify(ticket)
  end

  context 'when the user does not have a mobile number' do
    let(:mobile) { nil }

    it 'send an email' do
      expect(Email).to receive(:send_email_to_user)
      notification
    end

    it 'does not send an SMS' do
      expect(Sms).to_not receive(:send)
      notification
    end
  end
end

Tới đây chắc bạn đã có thể làm hài lòng khách hàng của mình mà đồng thời cũng thấy cấu trúc mô hình ứng dụng của mình trở lên "sáng sủa" hơn rất nhiều chứ!