Service Objects - Giải pháp cho các nghiệp vụ phức tạp trong Rails
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 ...
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.
Để 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 :
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ứ!