Tóm tắt
Các bước thực hiện refactoring Services Object trong API Rails APP viết bằng Grape
- Tạo service
- Xử lý exception, bắt lỗi
- Sử dụng service vào API
Nội dung
Sau khi hoàn thiện API bằng gem Grape thì mình đã refactor lại code với Service Object, tuy nhiên mình đã gặp một số vấn đề xử lý exception. Dưới đây là đoạn code của mình trước khi refactor, mình có 2 API: sendactivationemail và activate.
# This module is responsible for API with version 1.
module V1 # This class is responsible for Account Activation API with version 1. class AccountActivation < Grape::API resources :users do desc 'Send activation account email', { success: [{ code: 200, message: 'Send activation account email successfully' }], failure: [{ code: 400, message: 'Account has been activated' }, { code: 404, message: 'Not found User with email address' }] } params do requires :email, regexp: URI::MailTo::EMAIL_REGEXP end get '/send_activation_email' do user = User.find_by(email: params[:email]) error!('Not found User with email address', 404) unless user error!('Account has activated', 400) if user.activate? user.update!({ activation_digest: User.generate_unique_secure_token, activated_at: Time.zone.now }) user.send_activation_email end desc 'Activated the current user', { success: [{ code: 200, message: 'Activated account successfully' }], failure: [{ code: 400, message: 'Account has been activated' }, { code: 401, message: 'Failed to activate the current user' }, { code: 404, message: 'Not Found' }] } params do requires :activation_digest, type: String end get '/activate' do user = User.find_by(activation_digest: params[:activation_digest]) error!('Not found User! This link is not available now!', 400) unless user if user.activate_account_expired? error!('Activation Email has expired! Please try again in your email address', 410) end if user.activate? error!('Account has activated', 400) else user.activate! end present user, with: Entities::V1::UserFormat end end end
end
Đi refactor lại code
Tiếp theo mình tiến hành chia nhỏ các đoạn code trong API thành các Service để call lại. Mình tạo một thư mục services trong folder app , trong folder services tiếp tục tạo ra 3 file, cấu trúc thư mục như sau:
--- app
---------- services + application_service.rb -> lớp cha cho các service khác + activation_email_sender_service.rb -> service cho action send email trong API send_activation_email + activate_email_service.rb -> service cho action activate account trong API activate
Rồi, trong các file sẽ có gì? File application_service.rb
# This class is responsible for general service
class ApplicationService def self.call(*args) new(*args).call end
end
*File activation_email_sender_service.rb *
# This class is responsible for sending activation email
class ActivationEmailSenderService < ApplicationService def initialize(user) super() @user = user end def call send_activation_email end private def send_activation_email ** raise NotFoundError.new('Not found User with email address', 404) unless @user** ** raise ActivatedError.new('Account has activated', 400) if @user.activate?** @user.update!({ activation_digest: User.generate_unique_secure_token, activated_at: Time.zone.now }) UserMailer.account_activation(@user).deliver_now end
end
File activate_email_service.rb
# This class is responsible for activating account
class ActivateEmailService < ApplicationService def initialize(params) super() @activation_digest = params[:activation_digest] end def call activate_email end private def activate_email user = User.find_by(activation_digest: @activation_digest) ** raise NotFoundError.new('Not found User! This link is not available now', 404) unless user** if user.activate_account_expired? ** raise GoneError.new('Activation Email has expired! Please try again in your email address', 410)** end ** raise ActivatedError.new('Account has activated', 400) if user.activate?** user.activate! user end
end
Vì sao mình lại thiết kế code như vậy?
Đầu tiên, dựa theo nguyên tắc DRY (Don't Repeat Yourself) thì mình định nghĩa một lớp cha ApplicationService có hàm class function call truyền vào args và trong thân hàm gọi đến new(\args).call sẽ tạo object và gọi hàm call trong trong class (cụ thể ở đây là 2 class con ActivateEmailService và ActivationEmailSenderService).
Thứ hai, chú ý cho việc refactor là một Service Object có 1 public method và có nhiều private method, public method ở đây của mình là function call. Ví dụ trong file activate_email_service.rb, mình có một hàm call gọi tới private function activate_email. Mọi thứ xử lý logic mình bỏ trong private function đó hết.
Thứ ba, ở đây xuất hiện một số class mới như NotFoundError, ActivatedError,... *(mình để trong cặp dấu ** trong code) * mà mình call để raise exception. Đây là cách xử lý khi mình chuyển từ function error! trong Grape. Mình để code dưới đây để cho các bạn dễ so sánh hì.
get '/activate' do user = User.find_by(activation_digest: params[:activation_digest]) **error!('Not found User! This link is not available now!', 400) unless user** if user.activate_account_expired? ** error!('Activation Email has expired! Please try again in your email address', 410)** end if user.activate? **error!('Account has activated', 400)** else user.activate! end present user, with: Entities::V1::UserFormat end end
def activate_email user = User.find_by(activation_digest: @activation_digest) ** raise NotFoundError.new('Not found User! This link is not available now', 404) unless user** if user.activate_account_expired? ** raise GoneError.new('Activation Email has expired! Please try again in your email address', 410)** end ** raise ActivatedError.new('Account has activated', 400) if user.activate?** user.activate! user end
end
Tương quan code trong cặp dấu ** là cách mình xử lý exception. Lúc đầu mình tưởng rằng bê qua sẽ để i chang như vậy, ai mà có dè function error! là built-in method của class Grape::API, nên mình đâu dùng được. Vậy là mình phải đi build thêm mấy cái error đó nữa.
Mình đi tạo thêm folder errors trong folder app rồi tạo các file.rb trong trỏng. Cấu trúc thư mục như sau:
--- app
---------- services
---------- errors + application_error.rb + not_found_error.rb + activated_error.rb + inactivated_error.rb + gone_error.rb
Trong File application_error.rb, đây class cha cho mấy class error kia. Class này mình thừa kế từ StandardError class để từ nữa mình rescue lỗi (có 2 loại là StandardError vs Exception, mà Exception nó bắt hết nên không nên xài, tham khảo: StandardError vs Exception File application_error.rb
# This class is responsible for generating error
class ApplicationError < StandardError attr_reader :message, :code def initialize(message = 'Bad Request', code = 400) @message = message @code = code super(message) end
end
File not_found_error.rb
# This class is responsible 404 error
class NotFoundError < ApplicationError
end
Mấy class con kia tương tự he.
Nãy giờ là mình mới raise lỗi, giờ là lúc rescue lỗi trong API. Mình thêm dòng lệnh dưới đây vào các class API là được.
rescue_from :all do |e| error!({ error: e.class, message: e.message }, e.code) end
Cuối cùng, mình đi gọi các service của mình trong các API như sau:
ActivationEmailSenderService.call(user)
Kết quả như sau
Trong các class hiện thực API bằng Grape
# This module is responsible for API with version 1.
module V1 # This class is responsible for Account Activation API with version 1. class AccountActivation < Grape::API rescue_from :all do |e| error!({ error: e.class, message: e.message }, e.code) end resources :users do desc 'Send activation account email', { success: [{ code: 200, message: 'Send activation account email successfully' }], failure: [{ code: 400, message: 'Account has been activated' }, { code: 404, message: 'Not found User with email address' }] } params do requires :email, regexp: URI::MailTo::EMAIL_REGEXP end get '/send_activation_email' do user = User.find_by(email: params[:email]) ActivationEmailSenderService.call(user) end desc 'Activated the current user', { success: [{ code: 200, message: 'Activated account successfully' }], failure: [{ code: 400, message: 'Account has been activated' }, { code: 401, message: 'Failed to activate the current user' }, { code: 404, message: 'Not Found' }] } params do requires :activation_digest, type: String end get '/activate' do user = ActivateEmailService.call(params) present user, with: Entities::V1::UserFormat end end end
end
Tổng kết
Đó là một số vấn đề mình gặp phải khi refactor code. Hy vọng bài viết sẽ giúp ích được cho bạn.