Error Handling in Rails API - The Rails Way
Bài đăng này đã không được cập nhật trong 6 năm
Sau một thời gian dài làm việc chủ yếu với Javascript, nay mới có dịp trở lại với một dự án Ruby on Rails, lần này có dịp bắt tay vào buid dự án ngay từ đầu nên có nhiều vấn đề để cùng chia sẻ với mọi người.
Một trong những điều đầu tiên mình muốn chia sẻ cho mọi người là cách thức handle lỗi và resonse lỗi mang đậm chất Rails
Nói qua một chúc pattern mà mình sắp giới thiệu ở đây lấy cảm hứng từ bài hướng dẫn của tác giả Nguyễn Tấn Đức trong Rails API errors response và Handling Errors in an API Application the Rails Way của Łukasz Sarnacki.
Phần source code mẫu của bài viết này được publish tại github cá nhân của mình, bạn có thể clone về tham khảo: https://github.com/trandaison/rails_api_error_handling_best_practice/commits/happy_path
Đối với nhiều bạn khi lần đầu xây dựng một app API để phục vụ cho mobile client và web front-end vấn đề hay mắc phải nhất là định nghĩa response cho phía client.
Thường thì các vấn đề sau sẽ là điều khiến họ bối rối:
- Không biết format của response nên trả như thế nào cho hợp lý.
- Khi có lỗi trong quá trình thực thi, thông tin các trả về không đầy đủ.
- Xử lý không nhất quán đối với từng loại Error khác nhau bởi có quá muôn vàn kiểu lỗi.
- Nếu xử lý được thì code bị lặp hoặc không tối ưu, thậm chí code thối.
Mục tiêu của mình qua bài viết này là xây dựng một pattern mà trong các actions của controller hay trong hàm thực thi của các service object là một “happy path” và việc xử lý lỗi sẽ là thứ gì đó... nằm ở hậu cần, không thể tìm thấy trong code ở controller và service (nghe nguy hiểm nhỉ )
WTF is happy path?
Nó là một "con đường" lý tưởng trong actions của controller, ví dụ trong action update, con đường lý tưởng sẽ là: Tìm thấy object --> update attributes thành công --> render response, xem như chạy 1 mạch mà ko một lỗi lầm không cần phải kiểm tra rằng có tìm thấy record không, update có thành công hay không,...
Vậy vấn đề cần mổ xẻ ở đây là gì?
Với web app thì Rails hỗ trợ render error messages tận răng rồi, quá đơn giản, nhưng với API thì object lỗi mà Rails trả về cho client là một thứ bỏ đi
Tại sao à? Đơn giản là lỗi của active record validation trả về 1 đằng, còn các lỗi trong quá trình thực thi nó lại là một kiểu khác Làm cái gì cũng nên nghĩ cho client một tí, response gì mà khi thì thế này, khi khác thế khác thì ai "lập trình" cho thấu
Lôi thôi vậy để mọi người nắm được bối cảnh của vấn đề mà chúng ta sẽ giải quyết, còn bây giờ bước vào triển thôi, mình sẽ giải quyết từng vấn đề một một cách chi tiết nhất.
1. Giải quyết error validation của ActiveRecord
Nói luôn cho nó vuông đây là lỗi validation khi save
, update
hoặc gọi hàm valid?
trên một đối tượng của ActiveRecord
, là cái thứ lỗi mà bạn vẫn thường lấy thông qua cái hàm .errors()
quen thuộc
Trước hết mình xin phép đưa ra một example schema cho response, bao gồm 2 trường hợp:
- Trường hợp response success (thường đi kèm với HTTP status code
2xx
)
{
"success": true,
"data": {
"user": {
"email": "foo@bar.com",
"phone_number": "+841234567890"
}
}
}
- Trường hợp response error (Thường đi kèm với HTTP status code
4xxx
)
{
"success": false,
"errors": [
{
"resource": "user",
"field": "email",
"code": 1001,
"message": "Email has already been taken."
},
{
"resource": "user",
"field": "phone_number",
"code": 1002,
"message": "Phone number is invalid"
}
]
}
Mình giải thích qua một chút:
-
success
thuộc kiểuboolean
, làtrue
khi request đó thực hiện công việc thành công, còn lại làfalse
. -
data
là mộtobject
, đây là nơi chứa toàn bộ dữ liệu sẽ response cho client, keydata
được sử dụng khi giá trị củasuccess
làtrue
. -
errors
là mộtarray
, đây là nơi chứa toàn bộ những thông tin về lỗi, bao gồm cả lỗi validation lẫn lỗi trong quá trình thực thi. keyerrors
được sử dụng khi giá trị củasuccess
làfalse
.Trong đó từng object của mảng
errors
sẽ có cấu trúc như sau:resource
tên của model/resource bị lỗi, ở dạngstring
, định dạng snake case.field
tên của trường bị lỗi, cũng làstring
, định dạng snake case.code
* mã lỗi, là một số tuỳ bạn định nghĩa, client sẽ sử dụng mã này để phân biệt các lỗi. Mình thường dùng mã1xxx
.message
* Message lỗi, là mộtstring
, thông thường client sẽ sử dụng luôn message này để hiển thị ra cho người dùng.
Đầu tiên mình có một model User
được tạo ra từ scaffold
rails g scaffold User email password phone_number username full_name gender:integer age:integer
Mục đích là để tạo ra một vài errors cho việc test thử, nên mình sẽ thêm 1 tí validations vào model như sau:
class User < ApplicationRecord
validates :email, presence: true, format: /\A[a-zA-Z0-9_\-\.]+@(([a-zA-Z]+\.[a-zA-Z]+)|(([0-9]\.){3}[0-9]))\z/, uniqueness: true
validates :password, presence: true, format: /\A[^ ]{6,}\z/
validates :phone_number, presence: true, format: /\A\+84(1\d{9}|9\d{8})\z/, uniqueness: true
validates :username, presence: true, format: /\A[0-9a-zA-Z_\-\.]{6,}\z/
validates :full_name, presence: true, format: /\A[^!@#\$%\^&\*\(\)\+\{\}]{4,}\z/
validates :age, presence: true, numericality: {greater_than_or_equal_to: 18}
enum gender: %i|male female|
end
Xem qua một chút về action create
của users_controller
vừa được sinh ra:
def create
@user = User.new user_params
if @user.save
render json: @user, status: :created, location: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit :email, :password, :phone_number, :username, :full_name, :gender, :age
end
Humm... Trông code cũng không tệ nhỉ, nhưng một sự thật là trông nó rất xấu xí, vô tổ chức, và khả năng mở rộng về sau là không có nếu như cứ phải if else
kiểu đó .
Bạn có thể thử gọi API bằng Postman với form data cố tình làm cho fail validate, sẽ nhận được một response như sau:
{
"email": [
"is invalid"
],
"password": [
"can't be blank",
"is invalid"
],
"phone_number": [
"has already been taken"
],
"age": [
"can't be blank",
"is not a number"
]
}
Rõ ràng nó khác xa với những gì chúng ta cần như đã nói ở phía trên
Bây giờ bạn sẽ muốn sửa controller của mình để có một "happy path" đầu tiên như sau
def create
@user = User.create! user_params
render json: @user, status: :created, location: @user
end
Hãy thử gọi lại API trên Postman. BANG! Hô hô hô, bạn nhận được một cục lỗi Thứ gì đó đại loại như sau
Unprocessable Entity
ActiveRecord::RecordInvalid
Phải thôi, bạn vừa sửa controller của mình lại, sử dụng một bang method create!
thay vì create
.
Tốt thôi, hãy thử thêm đoạn code sau vào application_controller.rb
của bạn sau đó thử lại trên Postman.
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
protected
def render_unprocessable_entity_response error, status: :unprocessable_entity
render json: error.record.errors, status: status
end
end
Sao hả? Hết lỗi rồi nhé, bạn nhận được một response như cũ.
Bằng cách kết hợp bang method và rescue_from
bạn đã tạo ra được một happy path đầu tiên, controller trông sáng sủa hơn nhiều mà vẫn đạt được một response như nhau.
Sau khi đã catch
được lỗi rồi công việc đơn giản là phân tích lỗi để đưa về response chuẩn. Mình sẽ tạo ra một class
đảm nhận việc này và sửa hàm render_unprocessable_entity_response
trong application_controller
lại như sau:
def render_unprocessable_entity_response error, status: :unprocessable_entity
render json: Errors::ActiveRecordValidation.new(error.record).to_hash, status: status
end
Phải, vừa xuất hiện một class
lạ hoắc Errors::ActiveRecordValidation
, đây sẽ là class làm nhiệm vụ chuyển lỗi của validations thành format chuẩn của response.
Xem qua về object lỗi mà ActiveRecord
cung cấp để các bạn hình dung rõ hơn về đầu vào của chúng ta:
# error.record.errors.details
{:email=>[{:error=>:invalid, :value=>"tran.dai.sonframgia.com"}],
:password=>[{:error=>:blank}, {:error=>:invalid, :value=>nil}],
:phone_number=>[{:error=>:taken, :value=>"+841206213188"}],
:age=>[{:error=>:blank}, {:error=>:not_a_number, :value=>""}]}
# error.record.errors.to_hash true
{:email=>["Email is invalid"],
:password=>["Password can't be blank", "Password is invalid"],
:phone_number=>["Phone number has already been taken"],
:age=>["Age can't be blank", "Age is not a number"]}
Bây giờ định nghĩa class đó tại thư mục lib
: lib/errors/active_record_validation.rb
module Errors
class ActiveRecordValidation
attr_reader :record
def initialize record
@record = record
@errors = serialize
end
def serialize full_messages: true
messages = record.errors.to_hash full_messages
record.errors.details.map do |field, details|
detail = details.first[:error]
message = messages[field].first
ValidationErrorSerializer.new(record, field, detail, message).serialize
end
end
def to_hash
{
success: false,
errors: serialize
}
end
end
end
Hàm to_hash
sẽ tạo ra một hash với định dạng như mô tả ban đầu dùng cho render json ở application controller.
-
Trong hàm
serialize
bạn cần để ý dòngdetail = details.first[:error]
vàmessage = messages[field].first
, ở đây với mỗi trường bị lỗi chúng ta chỉ cần lấy một lỗi đầu tiên để trả về là đủ, client không hiện hết toàn bộ lỗi của một field lên đâu, nên trả về hết toàn bộ lỗi trên 1 trường là 1 ý tưởng thừa thải. -
ValidationErrorSerializer
nhìn cái tên class thôi cũng đủ biết nó là một cáiserilizer
rồi, nhiệm vụ của nó là từ các thông tin lỗi trả về thông tinresource
,field
,code
vàmessage
.Bạn có thể đặt class này ở thư mục
lib
, riêng mình mình thích gom các serilizers lại cùng 1 thư mục nên mình sẽ đặt ở đường dẫnapp/serializers/validation_error_serializer.rb
. Class này như sau:class ValidationErrorSerializer def initialize record, field, detail, message @record = record @field = field @detail = detail @message = message end def serialize { resource: resource, field: field, code: code, message: @message } end private def resource I18n.t underscored_resource_name, locale: :api, scope: [:api, :errors, :resources], default: underscored_resource_name end def field I18n.t @field, scope: [:api, :errors, :fields, underscored_resource_name], default: @field.to_s end def code I18n.t @detail, locale: :api, scope: [:api, :errors, :code], default: @detail.to_s end def underscored_resource_name @record.class.to_s.gsub("::", "").underscore end end
Trong này có một số chỗ bạn cần lưu ý:
-
resource
,field
,code
đều có thể đuợc custom lại text thông qua i18n. Tuy nhiên theo mình thì không nên custom lại, nên mình đặt các trường này vào filelocale
làapi
, tức là sẽ giống nhau giữa các ngôn ngữ. -
Mã lỗi
code
nếu không custom lại sẽ có dạng như sau:{ "resource": "user", "field": "email", "code": "invalid", "message": "Email is invalid" }
Nếu bạn hỏi mình để như vậy có được không? thì câu trả lời là được, rất dễ đọc, nhìn vào là biết lỗi gì liền thay vì nhìn con số
1009
, ai biết là lỗi quái gìNhưng không được Lỗi validation thì còn còn tên cho code chứ lỗi trong quá trình thực thi thì ai rảnh mà ngồi đặt tên cho nó, từ ngữ đâu diễn tả cho hết mà ko bị trùng. Nên thống nhất là dùng số cho tất cả các trường hợp.
Nghĩa là bạn phải tạo ra fileconfig/locales/api.yml
, mình sẽ cho bạn luôn toàn bộ lỗi củaActiveRecord
validation như sau:api: api: errors: code: default: 1000 confirmation: 1001 accepted: 1002 blank: 1003 present: 1004 too_short: 1005 too_long: 1006 wrong_length: 1007 taken: 1008 invalid: 1009 inclusion: 1010 exclusion: 1011 required: 1012 not_a_number: 1013 greater_than: 1014 greater_than_or_equal_to: 1015 equal_to: 1016 less_than: 1017 less_than_or_equal_to: 1018 other_than: 1019 not_an_integer: 1020 odd: 1021 even: 1022 record_not_found: 1100
-
Sau bước này, từ mớ hỗn độn ở trên mình đã tạo ra được một response đẹp đẽ hơn nhiều:
{
"success": false,
"errors": [
{
"resource": "user",
"field": "email",
"code": 1009,
"message": "Email is invalid"
},
{
"resource": "user",
"field": "password",
"code": 1003,
"message": "Password can't be blank"
},
{
"resource": "user",
"field": "phone_number",
"code": 1008,
"message": "Phone number has already been taken"
},
{
"resource": "user",
"field": "age",
"code": 1003,
"message": "Age can't be blank"
}
]
}
Chốt lại ở đây là sử dụng các bang method kết hợp với rescue_from
để handle lỗi thay vì các hàm save
, create
, update
,...
Vậy là xong việc cho ActiveRecord
validation rồi nhé
2. Một số lỗi khác của ActiveRecord
Ngoài ActiveRecord::RecordInvalid
ra thì còn có cả một mớ những class lỗi khác cần được rescue
nữa nếu trong quá trình code bạn có đụng đến. Lúc nào gặp phải thì bạn định nghĩa tương tự như cách làm ở trên là được rồi, mình không thể liệt kê hết được đâu, vì chính mình cũng chưa gặp hết toàn các lỗi đó, một số cái điển hình mình có thể liệt kê cho bạn như:
ActiveRecord::RecordNotDestroyed
ActiveRecord::RecordNotFound
ActiveRecord::RecordNotSaved
ActiveRecord::RecordNotUnique
Ở đây điển hình có ActiveRecord::RecordNotFound
chúng ta sẽ hay gặp phải (được raise
khi dùng hàm find
hoặc find_by!
)
Đầu tiên là rescue
nó ở application_controller
:
rescue_from ActiveRecord::RecordNotFound, with: :render_record_not_found_response
protected
def render_record_not_found_response error, status: :not_found
render json: Errors::ActiveRecordNotFound.new(error).to_hash, status: status
end
Lại ra đời thêm một class lỗi nữa. Bạn để ý một chút xíu lần này vẫn lại tạo ra một đối tượng lỗi rồi gọi hàm to_hash
để tạo ra body cho response. Khá quen nhỉ?
Ngoài ra việc body của response bao gồm errrors
và status
đồng nhất giữa các class lỗi, như vậy có phải là chúng ta nên vận dụng tính thừa kế của hướng đối tượng không nhỉ?
Đầu tiên tạo ra class lỗi để tất cả các class sau kế thừa nó lib/errors/application_error.rb
:
Nếu bạn có thắc mắc tại sao mình đặt tên là
application_error
thì cứ nhìn lại những gì rails (>= 5.1) đã cho ta,application_controller
,application_model
(không còn làActiveRecord::Base
nữa rồi nhé).Đến đây chắc bạn hiểu ý đồ của Rails và của mình rồi nhỉ?
module Errors
class ApplicationError < StandardError
attr_reader :code, :message
def initialize code: nil, message: nil
@code, @message = code, message
end
def serialize
[
{code: code, message: message}
]
end
def to_hash
{
success: false,
errors: serialize
}
end
end
end
Hàm khởi tạo của class này có 2 optional agruments là code
và message
, lý do mình sẽ giải thích ở phần 3.
Bây giờ mình có thể xoá hàm to_hash
ở ActiveRecordValidation
đi sau khi cho nó kế thừa ApplicationError
module Errors
class ActiveRecordValidation < Errors::ApplicationError
# all previous content is still here.
end
end
Tiếp theo là tạo ra file lib/errors/active_record_not_found.rb
, lưu ý là class này cũng sẽ kế thừa lớp Errors::ApplicationError
module Errors
class ActiveRecordNotFound < Errors::ApplicationError
attr_reader :model, :field, :detail, :message_key
def initialize error, message: nil
@model = error.model.underscore
@detail = error.class.to_s.split("::")[1].underscore
@field = error.primary_key
@message_key = message || :default
@errors = serialize
end
def serialize
[
{
resource: resource,
field: field,
code: code,
message: message
}
]
end
private
def message
I18n.t message_key,
scope: [:api, :errors, :messages, :not_found],
resource: resource
end
def resource
I18n.t model,
locale: :api,
scope: [:api, :errors, :resources],
default: model
end
def code
I18n.t detail,
locale: :api,
scope: [:api, :errors, :code],
default: detail
end
end
end
Bạn sẽ hỏi mình là có cần viết serializer cho thằng này không? Theo cá nhân mình thì không cần, bản thân lỗi này trong mảng errors
chỉ luôn có 1 phần tử thôi, tạo luôn phần tử đó ở đây là được rồi.
Vậy là giải quyết được thêm 1 lỗi phổ biến nữa rồi nhé. Các class error còn lại các bạn tự xoay sở
3. Giải quyết lỗi trong quá trình thực thi (controllers, service objects,...)
def passed_condition?
@errors = case false
when mission.present?
I18n.t "errors.complete_mission.mission_not_found"
when mission_user.present?
I18n.t "errors.complete_mission.current_missions_user_not_found"
when mission_award.present? || invalid_15th_mission_award?
I18n.t "errors.complete_mission.mission_award_not_found"
when !was_earned?
I18n.t "errors.complete_mission.mission_award_earned"
when !was_award_completion?
I18n.t "errors.complete_mission.mission_award_cannot_earn"
end
errors.nil?
end
WTF?
Hy vọng là bạn đã từng nghe nói qua về Service Object, hiểu nôm na nó như là osin cho controller vậy.
Bên trên là 1 đoạn code của một service object, hàm này được gọi trước khi thực thi object để đảm bảo rằng mọi thứ OK trước khi thực thi, và nếu có lỗi thì lỗi sẽ được gán cho biến @errors
, sau khi thực thi get errors
từ service object để response cho client.
Ai chọc mù mắt tôi đi Rõ ràng là rất khó để handle lỗi nếu có quá nhiều case như này.
Mọi thứ sẽ dễ chịu hơn nếu có Errors::ApplicationError
ở đây Thử hình dung cứ ở đâu gặp lỗi thì phóng lỗi ra tại đó ngay và luôn, dừng luôn việc thực thi lại, việc xử lý lỗi và response đã có 1 giàn hậu cần phía sau lo rồi, ko phải suy nghĩ gì thêm nữa. Khi đi đến được cuối hàm có nghĩa là làm đã thực thi thành công (vì nếu có lỗi thì đã bị rơi rụng giữa đường rồi).
Vậy lợi ích ở đây dễ dàng thấy là service object được tuân thủ "chỉ có 1 public method dùng để thực thi service", không cần method .success?
để kiểm tra service thực thi thành công hay không, ko cần method .errors
để lấy thông tin lỗi.
Trên thực thế lỗi ở service object và controller khá giống nhau, nên hướng giải quyết cũng như nhau.
Trở lại với class Errors::ApplicationError
, giả sử mình có một định nghĩa i18n
như sau:
en:
api:
errors:
unexpected:
code: 1100,
message: Opps... Unexpected error.
Mình có thể khởi tạo một đối tượng lỗi của ApplicationError
bằng thông tin ở i18n
nói trên như sau:
raise Errors::ApplicationError.new I18n.t(:unexpected, scope: [:api, :errors])
như vậy khi ở controller (hoặc service) gặp một lỗi nào đó bất kỳ mình muốn dừng chương trình lại và response lỗi đó về ngay cho client thì chỉ cần define lỗi đó vào file i18n
theo cặp gồm code
và message
, sau đó raise lỗi lên như ví dụ phía trên là xong.
Khá đẹp trai nhỉ Nhưng đẹp trai không chưa đủ, phải hot boy Mình muốn cách thức raise lỗi gọn gàng hơn nữa, cụ thể là như thế này:
raise Errors::ApplicationError, :unexpected
# or
# raise Errors::ApplicationError.new :unexpected
Ý tưởng là class lỗi sẽ tìm cách đọc xem lỗi ở đâu, lấy tên class đó và đọc vào file i18n
tương ứng.
Ví dụ mình raise
lỗi ở Api::V1::UsersController
thì đường dẫn ở file i18n
sẽ là "api.v1.users.<xxx>"
Để làm được điều này đầu tiên mình cần 1 class lỗi chung cho controller và service object, mình tạm gọi là lỗi runtime lib/errors/runtime.rb
module Errors
module Runtime
class StandarError < Errors::ApplicationError
attr_reader :type, :detail
def initialize type, detail
@type, @detail = type, detail
scope = i18n_scope
error = I18n.t detail, scope: scope, default: translation_missing(detail ,scope)
@code = error[:code]
@message = error[:message]
end
private
def i18n_scope
backtrace = caller 0, 5
matches_file = backtrace.last.match(file_path_regex) || backtrace[2].match(file_path_regex)
file_path = matches_file[0]
file_path.split(%r|/|)[3..-1].map {|e| e.gsub file_suffix, ""}
end
def file_path_regex
case type
when :controller
/\/app\/(controllers)\/.*\.rb/
when :service
/\/app\/(services)\/.*\.rb/
end
end
def file_suffix
case type
when :controller
%r|_controller.rb|
when :service
%r|_service.rb|
end
end
def translation_missing detail, scope
prefix_msg = "translation missing: #{scope.push(detail.to_s).join('.')}"
{
code: "#{prefix_msg}.code",
message: "#{prefix_msg}.message"
}
end
end
class ActionFailed < Errors::Runtime::StandarError
def initialize detail
super :controller, detail
end
end
class ServiceFailed < Errors::Runtime::StandarError
def initialize detail
super :service, detail
end
end
end
end
Để mình giải thích qua:
-
Class
StandarError
kế thừaErrors::ApplicationError
để sử dụng chung response format cũng như hàmto_hash
-
Bên dưới mình tạo ra thêm 2 class là
ActionFailed
dùng để bắn lỗi trong quá trình thực thi ở action trong controller, vàServiceFailed
dùng để bắn lỗi trong quá trình thực thi ở service object.Cả 2 class trên đều kế thừa
Errors::Runtime::StandarError
để sở hữu "tính năng" tự động dò tìmmessage
vàcode
ởi18n
. -
Cách thức dò tìm message ở
i18n
mình thực hiện dựa vàocaller()
, đây là một hàm trong moduleKernel
của ruby, bạn xem thêm tại đây, mình lợi dụng nó để tìm ra nơi nào đã bắn error (controller
hayservice
) và đọc đường dẫn đến file tương ứng để lấy message trongi18n
.
Bây giờ thay vì truyền vào code
và message
ngu ngốc ban đầu chúng ta chỉ cần truyền key cụ thể vào là đã lấy được code
và message
một cách tự động.
raise Errors::ApplicationError.new I18n.t(:unexpected, scope: [:api, :errors])
# same as
raise Errors::Runtime::ActionFailed, :unexpected # if in controller
# same as
raise Errors::Runtime::ServiceFailed, :unexpected # if in service
# same as
raise Errors::Runtime::StandarError.new(:controller, :unexpected) # if in controller
# same as
raise Errors::Runtime::StandarError.new(:service, :unexpected) # if in service
Tha hồ linh động sử dụng nhé.
Cuối cùng đừng quên thêm message
và code
ở i18n cũng như rescue_from
cho các class lỗi vừa mới thêm ở trên vào application controller của bạn nhé.
rescue_from(
Errors::Runtime::StandarError,
Errors::Runtime::ActionFailed,
Errors::Runtime::ServiceFailed,
with: :render_runtime_error_response
)
protected
def render_runtime_error_response error, status: :bad_request
render json: error.to_hash, status: status
end
Tổng kết: Còn điều gì cần giải quyết nữa không?
Còn, viết Test Cho đến thời điểm hiện tại mình chỉ mới viết Rspec cho controller, service để đảm bảo rằng code thực thi đúng, chứng tỏ những class lỗi này chạy đúng chứ mình chưa trực tiếp viết Rspec cho những class này, nếu bạn có ý tưởng nào đóng góp cho vấn đề này, feel free to comment below.
Còn nữa, khi số lượng class error tăng lên, đồng nghĩa với việc file application_controller
của bạn cũng sẽ phình ra, bạn có thể move những dòng code rescue_from
cũng những hàm handler sang một file khác, đặt trong thư mục app/controllers/concerns
, sau đó chỉ việc include
module concern vừa tạo vào application_controller
là xong.
Cuối cùng còn 2 điều mình muốn các bạn lưu ý:
-
Pattern này được lợi về mặt quản lý source code, giúp mọi thứ dễ dàng hơn, dễ hiểu hơn, bù lại nó raise nhiều exception hơn bình thường invoke nhiều error class hơn nên có thể sẽ bị hy sinh một chút về mặt performance. Mình vẫn chưa test cụ thể, sẽ update lại sau khi mình làm một bài test hiệu năng cho nó, nhưng trên thực tế mình cảm nhận tốc độ không có sự thay đổi.
Đối với những dự án lớn thì khả năng scale up và maintainability quan trọng hơn, phải hi sinh thôi.
-
Pattern này không phải thần thánh, nên có thể có trường hợp không cover hết được, tuy nhiên đến thời điểm hiện tại mình chưa thấy trường hợp nào ko dùng đc cứ yên tâm mà sử dụng thôi.
All rights reserved